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.
- kivyforge/__init__.py +9 -0
- kivyforge/artifacts/__init__.py +41 -0
- kivyforge/artifacts/cache.py +81 -0
- kivyforge/artifacts/collect.py +354 -0
- kivyforge/artifacts/download.py +125 -0
- kivyforge/artifacts/frameworks.py +240 -0
- kivyforge/artifacts/native_stage_util.py +346 -0
- kivyforge/artifacts/runtime.py +65 -0
- kivyforge/artifacts/verify.py +45 -0
- kivyforge/artifacts/wheels.py +89 -0
- kivyforge/bundle/__init__.py +1 -0
- kivyforge/bundle/pycompile.py +161 -0
- kivyforge/cli/__init__.py +52 -0
- kivyforge/cli/_common.py +62 -0
- kivyforge/cli/_platform.py +84 -0
- kivyforge/cli/build.py +111 -0
- kivyforge/cli/clean.py +135 -0
- kivyforge/cli/doctor.py +52 -0
- kivyforge/cli/init.py +612 -0
- kivyforge/cli/init_writer.py +716 -0
- kivyforge/cli/lock.py +261 -0
- kivyforge/cli/open_cmd.py +15 -0
- kivyforge/cli/package.py +134 -0
- kivyforge/cli/run.py +162 -0
- kivyforge/cli/status.py +20 -0
- kivyforge/cli/upgrade.py +314 -0
- kivyforge/config/__init__.py +46 -0
- kivyforge/config/errors.py +43 -0
- kivyforge/config/icons.py +60 -0
- kivyforge/config/loader.py +3079 -0
- kivyforge/config/model.py +817 -0
- kivyforge/doctor/__init__.py +19 -0
- kivyforge/doctor/checks_common.py +95 -0
- kivyforge/doctor/probe.py +357 -0
- kivyforge/doctor/result.py +41 -0
- kivyforge/lock/__init__.py +42 -0
- kivyforge/lock/_pip_shim.py +63 -0
- kivyforge/lock/find_links.py +182 -0
- kivyforge/lock/model.py +69 -0
- kivyforge/lock/pep751.py +111 -0
- kivyforge/lock/reader.py +30 -0
- kivyforge/lock/resolver.py +124 -0
- kivyforge/lock/wheelruntime/__init__.py +70 -0
- kivyforge/lock/wheelruntime/builder.py +280 -0
- kivyforge/lock/wheelruntime/model.py +94 -0
- kivyforge/lock/wheelruntime/native_binaries.py +127 -0
- kivyforge/lock/wheelruntime/pbs_github.py +133 -0
- kivyforge/lock/wheelruntime/profile.py +104 -0
- kivyforge/lock/wheelruntime/resolver.py +248 -0
- kivyforge/lock/wheelruntime/runtime.py +161 -0
- kivyforge/lock/wheelruntime/serialize.py +245 -0
- kivyforge/platforms/__init__.py +114 -0
- kivyforge/platforms/android/__init__.py +140 -0
- kivyforge/platforms/android/adb.py +215 -0
- kivyforge/platforms/android/bootstrap/__init__.py +7 -0
- kivyforge/platforms/android/bootstrap/contract.py +166 -0
- kivyforge/platforms/android/bootstrap/render.py +244 -0
- kivyforge/platforms/android/bootstrap/templates/androidtest/KivyforgeContractTest.java +76 -0
- kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivy_bootstrap.py +50 -0
- kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_bootstrap.py +54 -0
- kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_selftest.py +97 -0
- kivyforge/platforms/android/bootstrap/templates/cpp/CMakeLists.txt +20 -0
- kivyforge/platforms/android/bootstrap/templates/cpp/main.c +150 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/jnius/NativeInvocationHandler.java +48 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonActivity.java +75 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonBundle.java +132 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonService.java +189 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/renpy/android/Hardware.java +276 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/LICENSE-SDL.txt +18 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/SDL_REVISION.txt +3 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDevice.java +22 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceBLESteamController.java +650 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceManager.java +698 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceUSB.java +309 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDL.java +90 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLActivity.java +2120 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLAudioManager.java +514 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLControllerManager.java +856 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLSurface.java +405 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/LICENSE-SDL.txt +18 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/SDL_REVISION.txt +3 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDevice.java +21 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceBLESteamController.java +829 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceManager.java +698 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceUSB.java +354 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDL.java +90 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLActivity.java +2240 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLAudioManager.java +126 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLControllerManager.java +1010 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLDummyEdit.java +66 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLInputConnection.java +136 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSensorManager.java +73 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSurface.java +464 -0
- kivyforge/platforms/android/cli.py +1023 -0
- kivyforge/platforms/android/doctor.py +899 -0
- kivyforge/platforms/android/elf.py +148 -0
- kivyforge/platforms/android/generate/__init__.py +1 -0
- kivyforge/platforms/android/generate/manifest.py +261 -0
- kivyforge/platforms/android/generate/project.py +462 -0
- kivyforge/platforms/android/generate/services.py +187 -0
- kivyforge/platforms/android/gradle_wrapper/NOTICE +3 -0
- kivyforge/platforms/android/gradle_wrapper/SHA256SUMS +1 -0
- kivyforge/platforms/android/gradle_wrapper/gradle-wrapper.jar +0 -0
- kivyforge/platforms/android/gradle_wrapper/gradlew +164 -0
- kivyforge/platforms/android/gradle_wrapper/gradlew.bat +90 -0
- kivyforge/platforms/android/gradlew.py +60 -0
- kivyforge/platforms/android/icons.py +237 -0
- kivyforge/platforms/android/lock/__init__.py +1 -0
- kivyforge/platforms/android/lock/archives.py +109 -0
- kivyforge/platforms/android/lock/builder.py +387 -0
- kivyforge/platforms/android/lock/markers.py +71 -0
- kivyforge/platforms/android/lock/maven.py +252 -0
- kivyforge/platforms/android/lock/model.py +113 -0
- kivyforge/platforms/android/lock/python_meta.py +199 -0
- kivyforge/platforms/android/lock/reader.py +206 -0
- kivyforge/platforms/android/lock/resolver.py +276 -0
- kivyforge/platforms/android/lock/writer.py +121 -0
- kivyforge/platforms/android/policy.py +214 -0
- kivyforge/platforms/android/signing.py +145 -0
- kivyforge/platforms/android/smoke.py +43 -0
- kivyforge/platforms/android/splash.py +293 -0
- kivyforge/platforms/android/stage/__init__.py +1 -0
- kivyforge/platforms/android/stage/bundle.py +295 -0
- kivyforge/platforms/android/stage/jnilibs.py +186 -0
- kivyforge/platforms/android/stage/runtime.py +61 -0
- kivyforge/platforms/android/stage/wheels.py +106 -0
- kivyforge/platforms/android/toolchain.py +29 -0
- kivyforge/platforms/base.py +157 -0
- kivyforge/platforms/ios/__init__.py +129 -0
- kivyforge/platforms/ios/assets.py +193 -0
- kivyforge/platforms/ios/buildsettings.py +128 -0
- kivyforge/platforms/ios/cli.py +543 -0
- kivyforge/platforms/ios/doctor.py +410 -0
- kivyforge/platforms/ios/entitlements.py +34 -0
- kivyforge/platforms/ios/generator.py +318 -0
- kivyforge/platforms/ios/lock/__init__.py +71 -0
- kivyforge/platforms/ios/lock/builder.py +393 -0
- kivyforge/platforms/ios/lock/markers.py +69 -0
- kivyforge/platforms/ios/lock/model.py +95 -0
- kivyforge/platforms/ios/lock/python_meta.py +187 -0
- kivyforge/platforms/ios/lock/reader.py +191 -0
- kivyforge/platforms/ios/lock/resolver.py +304 -0
- kivyforge/platforms/ios/lock/spm.py +217 -0
- kivyforge/platforms/ios/lock/writer.py +114 -0
- kivyforge/platforms/ios/lock/xcframework.py +188 -0
- kivyforge/platforms/ios/materialize.py +59 -0
- kivyforge/platforms/ios/plist.py +91 -0
- kivyforge/platforms/ios/privacy.py +39 -0
- kivyforge/platforms/ios/skeleton.py +165 -0
- kivyforge/platforms/ios/sources.py +63 -0
- kivyforge/platforms/ios/staging.py +225 -0
- kivyforge/platforms/ios/swift_packages.py +324 -0
- kivyforge/platforms/ios/templates/kivyforge_bootstrap.h +34 -0
- kivyforge/platforms/ios/templates/kivyforge_bootstrap.m +146 -0
- kivyforge/platforms/ios/templates/main.m +15 -0
- kivyforge/platforms/ios/templates/main_config.h.in +12 -0
- kivyforge/platforms/ios/xcframeworks.py +194 -0
- kivyforge/platforms/ios/xcode/__init__.py +76 -0
- kivyforge/platforms/ios/xcode/commands.py +296 -0
- kivyforge/platforms/ios/xcode/runner.py +316 -0
- kivyforge/platforms/linux/__init__.py +120 -0
- kivyforge/platforms/linux/appimage.py +169 -0
- kivyforge/platforms/linux/bundle.py +235 -0
- kivyforge/platforms/linux/cli.py +232 -0
- kivyforge/platforms/linux/desktop.py +71 -0
- kivyforge/platforms/linux/doctor.py +438 -0
- kivyforge/platforms/linux/elftools.py +95 -0
- kivyforge/platforms/linux/icons.py +129 -0
- kivyforge/platforms/linux/launcher.py +107 -0
- kivyforge/platforms/linux/lock/__init__.py +126 -0
- kivyforge/platforms/linux/lock/profile.py +254 -0
- kivyforge/platforms/linux/lock/runtime.py +75 -0
- kivyforge/platforms/linux/native_stage.py +47 -0
- kivyforge/platforms/linux/runtime_stage.py +107 -0
- kivyforge/platforms/linux/wheels_stage.py +103 -0
- kivyforge/platforms/macos/__init__.py +121 -0
- kivyforge/platforms/macos/bundle.py +255 -0
- kivyforge/platforms/macos/cli.py +269 -0
- kivyforge/platforms/macos/doctor.py +480 -0
- kivyforge/platforms/macos/icns.py +75 -0
- kivyforge/platforms/macos/launcher.py +118 -0
- kivyforge/platforms/macos/lock/__init__.py +112 -0
- kivyforge/platforms/macos/lock/profile.py +119 -0
- kivyforge/platforms/macos/lock/runtime.py +58 -0
- kivyforge/platforms/macos/machotools.py +138 -0
- kivyforge/platforms/macos/native_stage.py +47 -0
- kivyforge/platforms/macos/notarize.py +143 -0
- kivyforge/platforms/macos/plist.py +37 -0
- kivyforge/platforms/macos/runtime_stage.py +110 -0
- kivyforge/platforms/macos/signing.py +78 -0
- kivyforge/platforms/macos/wheels_stage.py +108 -0
- kivyforge/platforms/windows/__init__.py +133 -0
- kivyforge/platforms/windows/assets.py +76 -0
- kivyforge/platforms/windows/bundle.py +298 -0
- kivyforge/platforms/windows/cli.py +314 -0
- kivyforge/platforms/windows/doctor.py +563 -0
- kivyforge/platforms/windows/fsswap.py +87 -0
- kivyforge/platforms/windows/icons.py +64 -0
- kivyforge/platforms/windows/launcher/__init__.py +193 -0
- kivyforge/platforms/windows/launcher/build_launcher.py +507 -0
- kivyforge/platforms/windows/launcher/launcher.c +379 -0
- kivyforge/platforms/windows/lock/__init__.py +112 -0
- kivyforge/platforms/windows/lock/profile.py +101 -0
- kivyforge/platforms/windows/lock/runtime.py +64 -0
- kivyforge/platforms/windows/naming.py +52 -0
- kivyforge/platforms/windows/native_stage.py +66 -0
- kivyforge/platforms/windows/petools.py +98 -0
- kivyforge/platforms/windows/rcedit.py +116 -0
- kivyforge/platforms/windows/runtime_stage.py +221 -0
- kivyforge/platforms/windows/signing.py +168 -0
- kivyforge/platforms/windows/vendor/LICENSE.rcedit +29 -0
- kivyforge/platforms/windows/vendor/NOTICE +17 -0
- kivyforge/platforms/windows/vendor/SHA256SUMS +4 -0
- kivyforge/platforms/windows/vendor/TOOLSET.txt +4 -0
- kivyforge/platforms/windows/vendor/__init__.py +12 -0
- kivyforge/platforms/windows/vendor/fetch_rcedit.py +97 -0
- kivyforge/platforms/windows/vendor/launcher-amd64.exe +0 -0
- kivyforge/platforms/windows/vendor/rcedit-x64.exe +0 -0
- kivyforge/platforms/windows/wheels_stage.py +139 -0
- kivyforge-3.0.0.dev0.dist-info/METADATA +609 -0
- kivyforge-3.0.0.dev0.dist-info/RECORD +225 -0
- kivyforge-3.0.0.dev0.dist-info/WHEEL +5 -0
- kivyforge-3.0.0.dev0.dist-info/entry_points.txt +3 -0
- kivyforge-3.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- 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
|