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,52 @@
1
+ """Windows-safe artifact naming (windows-spec).
2
+
3
+ The launcher ``<name>.exe`` and the packaged ``dist/windows`` folder name are
4
+ derived from ``[tool.kivy].display_name`` through :func:`windows_safe_name`,
5
+ which strips/replaces characters illegal in Windows filenames, avoids the
6
+ reserved DOS device names, and drops trailing dots/spaces (which Windows
7
+ silently removes). It is a pure helper so it is trivially unit-testable.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ # Characters that are illegal in a Windows filename component, plus control
15
+ # characters (0x00–0x1F). Backslash and forward slash are path separators.
16
+ _ILLEGAL = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
17
+
18
+ # Reserved DOS device names (case-insensitive). A component whose stem (the part
19
+ # before the first dot) is one of these is illegal even with an extension
20
+ # (``NUL.txt`` is still reserved).
21
+ _RESERVED = frozenset(
22
+ {
23
+ "CON",
24
+ "PRN",
25
+ "AUX",
26
+ "NUL",
27
+ *(f"COM{i}" for i in range(1, 10)),
28
+ *(f"LPT{i}" for i in range(1, 10)),
29
+ }
30
+ )
31
+
32
+
33
+ def windows_safe_name(display_name: str, *, fallback: str = "App") -> str:
34
+ """Return a Windows-filesystem-safe base name derived from ``display_name``.
35
+
36
+ Illegal characters become spaces (so ``"My|App"`` reads as ``"My App"``),
37
+ runs of whitespace collapse to a single space, and leading/trailing spaces
38
+ and trailing dots are removed. If the stem collides with a reserved device
39
+ name it is suffixed with ``_``. Returns ``fallback`` when nothing usable
40
+ remains (e.g. a name made entirely of illegal characters).
41
+ """
42
+ cleaned = _ILLEGAL.sub(" ", display_name)
43
+ # Collapse internal whitespace runs and strip the ends.
44
+ cleaned = " ".join(cleaned.split())
45
+ # Windows silently drops trailing dots and spaces from names.
46
+ cleaned = cleaned.rstrip(" .")
47
+ if not cleaned:
48
+ return fallback
49
+ stem = cleaned.split(".", 1)[0]
50
+ if stem.upper() in _RESERVED:
51
+ cleaned = f"{cleaned}_"
52
+ return cleaned
@@ -0,0 +1,66 @@
1
+ """Stage locked native binaries into the onedir ``bin\\`` (windows-spec).
2
+
3
+ The ``[tool.kivy.windows.native.binaries]`` channel: each entry is fetched +
4
+ SHA-256-verified from ``pylock.windows.toml`` (URL or vendored path) and staged
5
+ into ``<bundle>\\bin`` — a single native file, or a ``.zip`` / ``.tar.gz`` /
6
+ ``.tgz`` of several extracted with a path-traversal guard. ``bin\\`` is created
7
+ only when the lock actually pins binaries. The bootstrap registers ``<bundle>\\bin`` with
8
+ ``os.add_dll_directory`` and prepends it to ``PATH``.
9
+
10
+ The staging mechanics live in the shared
11
+ :mod:`kivyforge.artifacts.native_stage_util`; this is the Windows-facing wrapper
12
+ that translates :class:`NativeStageError` into :class:`WindowsBundleError` and
13
+ supplies the Windows-specific guards: case-insensitive collision keys, reserved
14
+ DOS device name / alternate-data-stream rejection, and a PE machine-architecture
15
+ check (:mod:`~kivyforge.platforms.windows.petools`) so a wrong-arch DLL is caught
16
+ at assembly time rather than at load.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from pathlib import Path
22
+
23
+ from kivyforge.artifacts.cache import ArtifactCache
24
+ from kivyforge.artifacts.native_stage_util import NativeStageError, stage_binaries
25
+
26
+ from . import WindowsBundleError
27
+ from .lock import WindowsLockfile
28
+ from .petools import PeArchError, verify_pe_arch
29
+
30
+
31
+ def stage_native_binaries(
32
+ lock: WindowsLockfile,
33
+ bundle: Path,
34
+ arch: str,
35
+ *,
36
+ project_root: Path,
37
+ cache: ArtifactCache | None = None,
38
+ no_cache: bool = False,
39
+ ) -> None:
40
+ """Stage every pinned native binary for the lock into ``<bundle>\\bin``.
41
+
42
+ Each staged PE is checked against *arch* (a wrong-architecture DLL fails the
43
+ build with a clear message); non-PE members (data files, scripts) are
44
+ ignored by the check.
45
+ """
46
+
47
+ def _validate(path: Path) -> None:
48
+ try:
49
+ verify_pe_arch(path, arch)
50
+ except PeArchError as exc:
51
+ raise NativeStageError(str(exc)) from exc
52
+
53
+ try:
54
+ stage_binaries(
55
+ lock.native_binaries,
56
+ bundle,
57
+ project_root=project_root,
58
+ cache=cache,
59
+ no_cache=no_cache,
60
+ bin_label="bin",
61
+ casefold=True,
62
+ reject_windows_names=True,
63
+ validate=_validate,
64
+ )
65
+ except NativeStageError as exc:
66
+ raise WindowsBundleError(str(exc)) from exc
@@ -0,0 +1,98 @@
1
+ """Minimal PE (Portable Executable) machine-type reader (windows-spec).
2
+
3
+ The PE analog of Linux ``elftools`` / macOS ``machotools``: a tiny, pure-Python
4
+ reader of the COFF header ``Machine`` field so a wrong-architecture DLL/EXE
5
+ staged into an amd64 app is caught with a clear message instead of a cryptic
6
+ load failure at runtime. Pure-Python (no ``ctypes``/OS APIs), so it runs on any
7
+ host and in hermetic unit tests.
8
+
9
+ Layout walked: ``MZ`` DOS signature at offset 0, the 4-byte PE-header offset at
10
+ ``0x3C``, the ``PE\\0\\0`` signature there, then the COFF header whose first
11
+ field is the 2-byte little-endian ``Machine``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import struct
17
+ from pathlib import Path
18
+
19
+ # IMAGE_FILE_MACHINE_* values (winnt.h).
20
+ IMAGE_FILE_MACHINE_I386 = 0x014C
21
+ IMAGE_FILE_MACHINE_AMD64 = 0x8664
22
+ IMAGE_FILE_MACHINE_ARM64 = 0xAA64
23
+ IMAGE_FILE_MACHINE_ARMNT = 0x01C4
24
+
25
+ # kivyforge arch name -> expected PE machine.
26
+ # arm64: already mapped — verify_pe_arch() validates arm64 native binaries today,
27
+ # so this module needs no change when win_arm64 lands. See arm64-windows.md §4.
28
+ _ARCH_MACHINE = {
29
+ "amd64": IMAGE_FILE_MACHINE_AMD64,
30
+ "arm64": IMAGE_FILE_MACHINE_ARM64,
31
+ }
32
+
33
+ _MACHINE_NAME = {
34
+ IMAGE_FILE_MACHINE_I386: "x86 (i386)",
35
+ IMAGE_FILE_MACHINE_AMD64: "x64 (amd64)",
36
+ IMAGE_FILE_MACHINE_ARM64: "arm64",
37
+ IMAGE_FILE_MACHINE_ARMNT: "arm (32-bit)",
38
+ }
39
+
40
+
41
+ def machine_name(machine: int) -> str:
42
+ """A human label for a PE ``Machine`` value (hex fallback if unknown)."""
43
+ return _MACHINE_NAME.get(machine, f"0x{machine:04x}")
44
+
45
+
46
+ def machine_for_arch(arch: str) -> int | None:
47
+ """The expected PE ``Machine`` for a kivyforge *arch* (``None`` if unknown)."""
48
+ return _ARCH_MACHINE.get(arch)
49
+
50
+
51
+ def read_pe_machine(path: Path) -> int | None:
52
+ """Return the PE ``Machine`` value of *path*, or ``None`` if it is not a PE.
53
+
54
+ Reads only the headers; never loads the file. Non-PE files (a ``.txt`` in a
55
+ zip, a truncated stub) return ``None`` so callers can skip them.
56
+ """
57
+ try:
58
+ with path.open("rb") as fh:
59
+ head = fh.read(0x40)
60
+ if len(head) < 0x40 or head[:2] != b"MZ":
61
+ return None
62
+ pe_offset = struct.unpack_from("<I", head, 0x3C)[0]
63
+ fh.seek(pe_offset)
64
+ sig = fh.read(6) # "PE\0\0" + first 2 bytes of Machine
65
+ if len(sig) < 6 or sig[:4] != b"PE\x00\x00":
66
+ return None
67
+ return struct.unpack_from("<H", sig, 4)[0]
68
+ except OSError:
69
+ return None
70
+
71
+
72
+ def is_pe(path: Path) -> bool:
73
+ """Whether *path* is a PE image (has a valid MZ + PE signature)."""
74
+ return read_pe_machine(path) is not None
75
+
76
+
77
+ class PeArchError(Exception):
78
+ """A staged PE image does not match the target architecture."""
79
+
80
+
81
+ def verify_pe_arch(path: Path, arch: str) -> None:
82
+ """Raise :class:`PeArchError` if *path* is a PE for a different *arch*.
83
+
84
+ Non-PE files are ignored (a native-binary channel may carry data files or
85
+ scripts). Unknown target archs are not enforced (forward-compat).
86
+ """
87
+ expected = machine_for_arch(arch)
88
+ if expected is None:
89
+ return
90
+ machine = read_pe_machine(path)
91
+ if machine is None or machine == expected:
92
+ return
93
+ raise PeArchError(
94
+ f"{path.name} is a {machine_name(machine)} PE image, but the app targets "
95
+ f"{arch} ({machine_name(expected)}).\n"
96
+ " Ship the matching-architecture binary in "
97
+ "[tool.kivy.windows.native.binaries]."
98
+ )
@@ -0,0 +1,116 @@
1
+ """Per-app launcher resource patching via the vendored ``rcedit`` (windows-spec).
2
+
3
+ The single vendored launcher is parameterized per app only by its *resources*
4
+ (no recompilation): the icon (from ``[tool.kivy.windows.icons]``) and the
5
+ version resource (product/file version, names, copyright — from ``[project]`` /
6
+ ``[tool.kivy]``). ``rcedit`` performs the edit without any toolchain.
7
+
8
+ Command construction (:func:`build_rcedit_args`) is pure and host-agnostic so it
9
+ is unit-tested directly; execution (:func:`patch_resources`) is Windows-only.
10
+ Patching happens **before** signing — a resource edit invalidates any existing
11
+ Authenticode signature (the signing pipeline guarantees the order).
12
+
13
+ The numeric ``FILEVERSION``/``PRODUCTVERSION`` are four 16-bit integers; the
14
+ PEP 440 → four-part mapping lives in signing (Phase 9) and is passed in here as
15
+ an already-formatted ``a.b.c.d`` string.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import subprocess
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+
24
+ from . import WindowsBundleError
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class ResourcePatch:
29
+ """The per-app resources to write into a launcher copy.
30
+
31
+ Every field is optional: only the ones set emit an ``rcedit`` flag, so a
32
+ minimal project (no icon, no metadata) still produces a valid launcher.
33
+ ``file_version``/``product_version`` are the numeric ``a.b.c.d`` forms;
34
+ ``product_version_string`` carries the human PEP 440 string.
35
+ """
36
+
37
+ icon: Path | None = None
38
+ product_name: str | None = None
39
+ file_description: str | None = None
40
+ product_version_string: str | None = None
41
+ legal_copyright: str | None = None
42
+ company_name: str | None = None
43
+ file_version: str | None = None # numeric "a.b.c.d"
44
+ product_version: str | None = None # numeric "a.b.c.d"
45
+
46
+ @property
47
+ def is_empty(self) -> bool:
48
+ return all(
49
+ getattr(self, f) is None
50
+ for f in (
51
+ "icon",
52
+ "product_name",
53
+ "file_description",
54
+ "product_version_string",
55
+ "legal_copyright",
56
+ "company_name",
57
+ "file_version",
58
+ "product_version",
59
+ )
60
+ )
61
+
62
+
63
+ def build_rcedit_args(rcedit: Path, exe: Path, patch: ResourcePatch) -> list[str]:
64
+ """The full ``rcedit`` argv to patch *exe* in place (one invocation).
65
+
66
+ ``rcedit`` applies every ``--set-*`` operation given in a single call, so
67
+ the whole patch is one command. Returns ``[rcedit, exe]`` (no-op) when the
68
+ patch is empty — callers may skip execution in that case.
69
+ """
70
+ args: list[str] = [str(rcedit), str(exe)]
71
+ if patch.icon is not None:
72
+ args += ["--set-icon", str(patch.icon)]
73
+ # Numeric version fields first (dedicated flags), then string fields.
74
+ if patch.file_version is not None:
75
+ args += ["--set-file-version", patch.file_version]
76
+ if patch.product_version is not None:
77
+ args += ["--set-product-version", patch.product_version]
78
+ string_fields = [
79
+ ("ProductName", patch.product_name),
80
+ ("FileDescription", patch.file_description),
81
+ ("ProductVersion", patch.product_version_string),
82
+ ("LegalCopyright", patch.legal_copyright),
83
+ ("CompanyName", patch.company_name),
84
+ ]
85
+ for key, value in string_fields:
86
+ if value is not None:
87
+ args += ["--set-version-string", key, value]
88
+ return args
89
+
90
+
91
+ def patch_resources(
92
+ exe: Path, patch: ResourcePatch, *, rcedit: Path | None = None
93
+ ) -> None:
94
+ """Patch *exe*'s icon/version resources in place with ``rcedit`` (Windows).
95
+
96
+ A no-op when *patch* is empty. Raises :class:`WindowsBundleError` on any
97
+ ``rcedit`` failure.
98
+ """
99
+ if patch.is_empty:
100
+ return
101
+ if rcedit is None:
102
+ from .assets import vendored_rcedit
103
+
104
+ rcedit = vendored_rcedit()
105
+ args = build_rcedit_args(rcedit, exe, patch)
106
+ try:
107
+ proc = subprocess.run(args, capture_output=True, text=True)
108
+ except OSError as exc:
109
+ raise WindowsBundleError(
110
+ f"failed to run rcedit ({rcedit}): {exc}. rcedit is Windows-only."
111
+ ) from exc
112
+ if proc.returncode != 0:
113
+ raise WindowsBundleError(
114
+ f"rcedit failed to patch {exe.name} (exit {proc.returncode}):\n"
115
+ f"{(proc.stderr or proc.stdout).strip()}"
116
+ )
@@ -0,0 +1,221 @@
1
+ """Stage the bundled CPython runtime into the onedir ``python\\`` prefix.
2
+
3
+ Consumes the provider-agnostic runtime pin from ``pylock.windows.toml`` (a single
4
+ ``amd64`` artifact this phase), fetches + SHA-256-verifies its archive, extracts
5
+ it, and materializes the **whole** python-build-standalone prefix at
6
+ ``<bundle>\\python`` — never flattened or pruned. The Windows PBS layout keeps
7
+ ``python.exe`` at the prefix root alongside ``python3xx.dll``, with ``DLLs\\``,
8
+ ``Lib\\``, ``Scripts\\`` etc. beneath it; the launcher points ``PYTHONHOME`` here.
9
+
10
+ VC++ runtime (Phase 0 finding — resolved): the PBS ``install_only`` Windows
11
+ archive ships ``vcruntime140.dll`` and ``vcruntime140_1.dll`` next to
12
+ ``python.exe`` but **not** ``msvcp140.dll`` (the C++ runtime some native deps
13
+ need). :func:`ensure_vc_runtime` verifies the core CPython DLLs are present
14
+ (a no-op for stock PBS, with an app-local fallback should a future PBS build
15
+ drop them) and best-effort places ``msvcp140.dll`` app-local when the runtime
16
+ itself omits it — never relying on a system-installed VCRedist, which is the
17
+ classic clean-machine failure.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import shutil
24
+ import struct
25
+ import tarfile
26
+ import tempfile
27
+ from pathlib import Path
28
+
29
+ from kivyforge.artifacts.cache import ArtifactCache
30
+ from kivyforge.artifacts.download import DownloadError, fetch_artifact
31
+ from kivyforge.lock.wheelruntime.model import PythonRuntime
32
+ from kivyforge.lock.wheelruntime.runtime import (
33
+ RuntimeProviderError,
34
+ normalized_runtime_root,
35
+ )
36
+
37
+ from . import WindowsBundleError
38
+ from .petools import machine_for_arch, read_pe_machine
39
+
40
+ # CPython itself links these; without them python.exe will not start. PBS ships
41
+ # them, so the check is normally a no-op.
42
+ CORE_VC_RUNTIME = ("vcruntime140.dll", "vcruntime140_1.dll")
43
+ # The C++ runtime; PBS does NOT ship it. Native deps (SDL, codecs) may need it.
44
+ # Placed app-local best-effort when neither the runtime nor a dep provides it.
45
+ CXX_VC_RUNTIME = "msvcp140.dll"
46
+
47
+ # The host location the redistributable VC++ runtime DLLs live in, used only as
48
+ # a fallback source when the runtime archive omits one.
49
+ DEFAULT_SYSTEM_DIR = Path(r"C:\Windows\System32")
50
+
51
+
52
+ def stage_runtime(
53
+ runtime: PythonRuntime,
54
+ arch: str,
55
+ home: Path,
56
+ *,
57
+ project_root: Path,
58
+ cache: ArtifactCache | None = None,
59
+ no_cache: bool = False,
60
+ system_dir: Path | None = None,
61
+ ) -> Path:
62
+ """Materialize the whole runtime prefix for *arch* at *home*; return *home*.
63
+
64
+ *home* is the final CPython prefix (``<bundle>\\python``). It is emptied and
65
+ recreated, then the conditional VC-runtime step runs.
66
+ """
67
+ cache = cache or ArtifactCache()
68
+ artifact = runtime.artifact_for(arch)
69
+ if artifact is None:
70
+ raise WindowsBundleError(
71
+ f"the lock has no {arch} runtime artifact; re-run "
72
+ f"`kivyforge lock -p windows` (locked archs: "
73
+ f"{', '.join(a.arch for a in runtime.artifacts)})."
74
+ )
75
+
76
+ tmp = Path(tempfile.mkdtemp(prefix="kivy-runtime-"))
77
+ try:
78
+ archive = _fetch(artifact, arch, project_root, cache, no_cache)
79
+ extracted = _extract(archive, tmp, runtime.provider)
80
+ if home.exists():
81
+ shutil.rmtree(home)
82
+ home.parent.mkdir(parents=True, exist_ok=True)
83
+ shutil.copytree(extracted, home, symlinks=True)
84
+ finally:
85
+ shutil.rmtree(tmp, ignore_errors=True)
86
+
87
+ ensure_vc_runtime(home, arch=arch, system_dir=system_dir)
88
+ return home
89
+
90
+
91
+ def ensure_vc_runtime(
92
+ home: Path, *, arch: str = "amd64", system_dir: Path | None = None
93
+ ) -> tuple[str, ...]:
94
+ """Guarantee the VC++ runtime DLLs the prefix needs sit next to python.exe.
95
+
96
+ Returns the DLLs this step placed app-local (empty when the runtime already
97
+ provides everything, the common PBS case). Raises if a *core* CPython DLL is
98
+ missing and cannot be sourced — python.exe could not start otherwise.
99
+
100
+ Any DLL taken from the host fallback is checked against *arch*: under a
101
+ 32-bit Python process ``C:\\Windows\\System32`` is WOW64-redirected to the
102
+ x86 ``SysWOW64``, so a naive copy could put an x86 runtime into an amd64
103
+ bundle. Mismatched host DLLs are refused (core) or skipped (msvcp140).
104
+ """
105
+ source = system_dir if system_dir is not None else _default_system_dir()
106
+ placed: list[str] = []
107
+
108
+ for dll in CORE_VC_RUNTIME:
109
+ if (home / dll).is_file():
110
+ continue
111
+ if _copy_from(source, dll, home, arch):
112
+ placed.append(dll)
113
+ else:
114
+ raise WindowsBundleError(
115
+ f"the staged runtime is missing {dll} and no {arch} copy was "
116
+ f"found in {source}. python.exe cannot start without the VC++ "
117
+ "runtime; the runtime archive or a matching-architecture VC++ "
118
+ "redistributable is required."
119
+ )
120
+
121
+ # Best-effort: only place the C++ runtime app-local when the runtime omits
122
+ # it. A dep wheel may still ship it into site-packages; that is fine.
123
+ if not (home / CXX_VC_RUNTIME).is_file() and _copy_from(
124
+ source, CXX_VC_RUNTIME, home, arch
125
+ ):
126
+ placed.append(CXX_VC_RUNTIME)
127
+
128
+ return tuple(placed)
129
+
130
+
131
+ def _copy_from(source: Path, name: str, home: Path, arch: str) -> bool:
132
+ """Copy *source*/*name* to *home* iff it matches *arch*; return if copied.
133
+
134
+ A host DLL whose PE machine type does not match the target architecture is
135
+ treated as absent — never copied — so a WOW64-redirected (x86) fallback or
136
+ any other wrong-arch source cannot poison the bundle.
137
+ """
138
+ candidate = source / name
139
+ if not candidate.is_file():
140
+ return False
141
+ expected = machine_for_arch(arch)
142
+ machine = read_pe_machine(candidate)
143
+ if expected is not None and machine is not None and machine != expected:
144
+ return False
145
+ shutil.copy2(candidate, home / name)
146
+ return True
147
+
148
+
149
+ def _default_system_dir() -> Path:
150
+ """The host source for VC-runtime fallbacks, WOW64-redirection aware.
151
+
152
+ A 32-bit Python process has ``System32`` file-system-redirected to the x86
153
+ ``SysWOW64``; the ``Sysnative`` pseudo-directory reaches the real 64-bit
154
+ ``System32`` from such a process. On a 64-bit interpreter ``System32`` is
155
+ already correct.
156
+ """
157
+ # arm64: this WOW64 logic is x86/x64-specific and does not generalize to
158
+ # Arm64 Windows (the WOW layer there is Arm32 SysArm32, plus x64 emulation).
159
+ # The host-System32 fallback only ever works same-arch: cross-building an
160
+ # arm64 bundle on an x64 host cannot source an arm64 msvcp140.dll here (the
161
+ # _copy_from PE-machine guard correctly refuses the x64 one). The intended
162
+ # fix is NOT to teach this function about arm64 System32 — it is to pin
163
+ # msvcp140.dll as a per-arch, SHA-256-verified artifact and retire this
164
+ # host fallback, which is what makes cross-arch builds host-independent.
165
+ # See arm64-windows.md §6, §7.
166
+ if struct.calcsize("P") == 4: # 32-bit interpreter
167
+ root = Path(os.environ.get("SystemRoot", r"C:\Windows"))
168
+ sysnative = root / "Sysnative"
169
+ if sysnative.is_dir():
170
+ return sysnative
171
+ return DEFAULT_SYSTEM_DIR
172
+
173
+
174
+ def _fetch(artifact, arch, project_root, cache, no_cache) -> Path:
175
+ filename = artifact.url.rsplit("/", 1)[-1] or f"python-{arch}.tar.gz"
176
+ try:
177
+ return fetch_artifact(
178
+ name=f"python runtime ({arch})",
179
+ sha256=artifact.sha256,
180
+ filename=filename,
181
+ url=artifact.url,
182
+ project_root=project_root,
183
+ cache=cache,
184
+ no_cache=no_cache,
185
+ )
186
+ except DownloadError as exc:
187
+ raise WindowsBundleError(str(exc)) from exc
188
+
189
+
190
+ def _extract(archive: Path, into: Path, provider: str) -> Path:
191
+ into.mkdir(parents=True, exist_ok=True)
192
+ try:
193
+ with tarfile.open(archive, "r:*") as tf:
194
+ _safe_extractall(tf, into)
195
+ except (tarfile.TarError, OSError) as exc:
196
+ raise WindowsBundleError(f"failed to extract {archive.name}: {exc}") from exc
197
+ try:
198
+ return normalized_runtime_root(provider, into)
199
+ except RuntimeProviderError as exc:
200
+ raise WindowsBundleError(f"{archive.name}: {exc}") from exc
201
+
202
+
203
+ def _safe_extractall(tf: tarfile.TarFile, into: Path) -> None:
204
+ """Extract, rejecting members that would escape *into*.
205
+
206
+ The explicit name check gives a clear message for the common ``../`` case;
207
+ ``filter="data"`` then provides the protection the name check cannot — it
208
+ validates symlink/hardlink *targets* (a safely named link pointing outside,
209
+ written through by a later member, would otherwise escape) and strips device
210
+ nodes and other special files. ``filter="fully_trusted"`` restored links
211
+ verbatim and did none of this.
212
+ """
213
+ base = into.resolve()
214
+ for member in tf.getmembers():
215
+ target = (into / member.name).resolve()
216
+ if not (target == base or base in target.parents):
217
+ raise WindowsBundleError(f"unsafe path in archive: {member.name!r}")
218
+ try:
219
+ tf.extractall(into, filter="data") # noqa: S202 — hardened filter (validates links)
220
+ except tarfile.FilterError as exc:
221
+ raise WindowsBundleError(f"unsafe member in archive: {exc}") from exc