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
kivyforge/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""kivyforge: a declarative iOS bundler for Kivy apps.
|
|
2
|
+
|
|
3
|
+
kivyforge 3.0 reads a user-authored ``pyproject.toml`` (PEP 621 ``[project]``
|
|
4
|
+
plus ``[tool.kivy]`` / ``[tool.kivy.ios]``), resolves a PEP 751
|
|
5
|
+
``pylock.ios.toml`` build manifest, and materializes it into a generated
|
|
6
|
+
Xcode project. See ``docs/proposals`` for the full specification.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "3.0.0.dev0"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""kivyforge artifact layer: fetch/cache/verify, wheels, frameworks, runtimes (spec 03)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .cache import ArtifactCache
|
|
6
|
+
from .download import Downloader, DownloadError, UrllibDownloader, fetch_artifact
|
|
7
|
+
from .frameworks import (
|
|
8
|
+
FrameworkConflict,
|
|
9
|
+
copy_wheel_frameworks,
|
|
10
|
+
extract_xcframework_archive,
|
|
11
|
+
)
|
|
12
|
+
from .runtime import (
|
|
13
|
+
PythonOrgRuntime,
|
|
14
|
+
PythonRuntime,
|
|
15
|
+
RuntimeArtifact,
|
|
16
|
+
get_runtime,
|
|
17
|
+
)
|
|
18
|
+
from .verify import HashMismatch, sha256_bytes, sha256_file, verify_file
|
|
19
|
+
from .wheels import BuildSlice, WheelSelectionError, select_wheel
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ArtifactCache",
|
|
23
|
+
"DownloadError",
|
|
24
|
+
"Downloader",
|
|
25
|
+
"UrllibDownloader",
|
|
26
|
+
"fetch_artifact",
|
|
27
|
+
"FrameworkConflict",
|
|
28
|
+
"copy_wheel_frameworks",
|
|
29
|
+
"extract_xcframework_archive",
|
|
30
|
+
"PythonOrgRuntime",
|
|
31
|
+
"PythonRuntime",
|
|
32
|
+
"RuntimeArtifact",
|
|
33
|
+
"get_runtime",
|
|
34
|
+
"HashMismatch",
|
|
35
|
+
"sha256_bytes",
|
|
36
|
+
"sha256_file",
|
|
37
|
+
"verify_file",
|
|
38
|
+
"BuildSlice",
|
|
39
|
+
"WheelSelectionError",
|
|
40
|
+
"select_wheel",
|
|
41
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Content-addressed artifact cache (spec 03).
|
|
2
|
+
|
|
3
|
+
Artifacts (wheels, xcframework archives, Python.xcframework) are cached by
|
|
4
|
+
SHA-256 in a per-user, OS-appropriate cache directory so repeated builds and
|
|
5
|
+
``--no-cache`` opt-outs are cheap and deterministic. The cache key is the
|
|
6
|
+
content hash, so a tampered/replaced upstream artifact can never silently
|
|
7
|
+
satisfy a cache hit for a different hash.
|
|
8
|
+
|
|
9
|
+
The cache root follows each host's convention: ``~/Library/Caches/kivyforge``
|
|
10
|
+
on macOS, ``$XDG_CACHE_HOME/kivyforge`` (default ``~/.cache/kivyforge``) on
|
|
11
|
+
Linux and other Unixes, and ``%LOCALAPPDATA%\\kivyforge\\Cache`` on Windows.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import platform as _platform
|
|
18
|
+
import shutil
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
APP_NAME = "kivyforge"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _default_cache_root() -> Path:
|
|
25
|
+
"""The per-user artifact cache directory for the current host OS."""
|
|
26
|
+
system = _platform.system()
|
|
27
|
+
if system == "Darwin":
|
|
28
|
+
return Path.home() / "Library" / "Caches" / APP_NAME / "artifacts"
|
|
29
|
+
if system == "Windows":
|
|
30
|
+
base = os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")
|
|
31
|
+
return Path(base) / APP_NAME / "Cache" / "artifacts"
|
|
32
|
+
# Linux and other Unixes: the XDG Base Directory spec.
|
|
33
|
+
xdg = os.environ.get("XDG_CACHE_HOME")
|
|
34
|
+
base = Path(xdg) if xdg else (Path.home() / ".cache")
|
|
35
|
+
return base / APP_NAME / "artifacts"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
DEFAULT_CACHE_ROOT = _default_cache_root()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ArtifactCache:
|
|
42
|
+
def __init__(self, root: str | Path | None = None) -> None:
|
|
43
|
+
self.root = DEFAULT_CACHE_ROOT if root is None else Path(root)
|
|
44
|
+
|
|
45
|
+
def path_for(self, sha256: str, filename: str) -> Path:
|
|
46
|
+
# Shard by the first two hex chars to keep directories small.
|
|
47
|
+
return self.root / sha256[:2] / f"{sha256}-{filename}"
|
|
48
|
+
|
|
49
|
+
def get(self, sha256: str, filename: str) -> Path | None:
|
|
50
|
+
candidate = self.path_for(sha256, filename)
|
|
51
|
+
return candidate if candidate.is_file() else None
|
|
52
|
+
|
|
53
|
+
def find_by_filename(self, filename: str) -> Path | None:
|
|
54
|
+
"""Return a cached artifact path matching ``{sha256}-{filename}``, if any."""
|
|
55
|
+
if not self.root.is_dir():
|
|
56
|
+
return None
|
|
57
|
+
suffix = f"-{filename}"
|
|
58
|
+
for shard in self.root.iterdir():
|
|
59
|
+
if not shard.is_dir():
|
|
60
|
+
continue
|
|
61
|
+
for path in shard.iterdir():
|
|
62
|
+
if path.is_file() and path.name.endswith(suffix):
|
|
63
|
+
return path
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def put_file(self, src: str | Path, sha256: str, filename: str) -> Path:
|
|
67
|
+
dest = self.path_for(sha256, filename)
|
|
68
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
if not dest.is_file():
|
|
70
|
+
shutil.copyfile(src, dest)
|
|
71
|
+
return dest
|
|
72
|
+
|
|
73
|
+
def put_bytes(self, data: bytes, sha256: str, filename: str) -> Path:
|
|
74
|
+
dest = self.path_for(sha256, filename)
|
|
75
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
dest.write_bytes(data)
|
|
77
|
+
return dest
|
|
78
|
+
|
|
79
|
+
def clear(self) -> None:
|
|
80
|
+
if self.root.is_dir():
|
|
81
|
+
shutil.rmtree(self.root)
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Collect lockfile artifacts into the staging tree (build steps 2-5, spec 06).
|
|
2
|
+
|
|
3
|
+
Downloads/verifies the Python.xcframework, installs pinned wheels into
|
|
4
|
+
``pip-deps/``, copies wheel-embedded ``.frameworks/`` into ``Frameworks/``, and
|
|
5
|
+
extracts user-declared native xcframeworks. The subprocess runner is injectable
|
|
6
|
+
so the orchestration can be unit-tested without invoking pip.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tarfile
|
|
15
|
+
import tempfile
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from ..bundle.pycompile import PycompileError, byte_compile, select_compiler
|
|
20
|
+
from ..config.model import DesktopBuildSettings
|
|
21
|
+
from ..lock.model import LockedWheel
|
|
22
|
+
from ..platforms.ios.lock.model import Lockfile
|
|
23
|
+
from .cache import ArtifactCache
|
|
24
|
+
from .download import Downloader, fetch_artifact
|
|
25
|
+
from .frameworks import (
|
|
26
|
+
PlacedFramework,
|
|
27
|
+
copy_wheel_frameworks,
|
|
28
|
+
extract_xcframework_archive,
|
|
29
|
+
)
|
|
30
|
+
from .wheels import BuildSlice, select_wheel
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CollectError(Exception):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def pip_install_command(
|
|
38
|
+
wheel_files: list[str],
|
|
39
|
+
*,
|
|
40
|
+
target_dir: str,
|
|
41
|
+
platform_tag: str,
|
|
42
|
+
python_version: str,
|
|
43
|
+
abi: str,
|
|
44
|
+
python_executable: str | None = None,
|
|
45
|
+
) -> list[str]:
|
|
46
|
+
"""Build the pip cross-install command (spec 05 step 4); no re-resolution."""
|
|
47
|
+
py = python_executable or sys.executable
|
|
48
|
+
cmd = [
|
|
49
|
+
py,
|
|
50
|
+
"-m",
|
|
51
|
+
"pip",
|
|
52
|
+
"install",
|
|
53
|
+
"--no-deps",
|
|
54
|
+
"--only-binary=:all:",
|
|
55
|
+
"--platform",
|
|
56
|
+
platform_tag,
|
|
57
|
+
"--python-version",
|
|
58
|
+
python_version,
|
|
59
|
+
"--abi",
|
|
60
|
+
abi,
|
|
61
|
+
"--implementation",
|
|
62
|
+
"cp",
|
|
63
|
+
"--target",
|
|
64
|
+
target_dir,
|
|
65
|
+
]
|
|
66
|
+
cmd += wheel_files
|
|
67
|
+
return cmd
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def collect_artifacts(
|
|
71
|
+
lock: Lockfile,
|
|
72
|
+
layout,
|
|
73
|
+
*,
|
|
74
|
+
build_slices: list[BuildSlice],
|
|
75
|
+
project_root: Path,
|
|
76
|
+
cache: ArtifactCache | None = None,
|
|
77
|
+
downloader: Downloader | None = None,
|
|
78
|
+
no_cache: bool = False,
|
|
79
|
+
runner=subprocess.run,
|
|
80
|
+
python_executable: str | None = None,
|
|
81
|
+
release: bool = False,
|
|
82
|
+
build_settings: DesktopBuildSettings | None = None,
|
|
83
|
+
echo: Callable[[str], None] = lambda msg: None,
|
|
84
|
+
) -> None:
|
|
85
|
+
cache = cache or ArtifactCache()
|
|
86
|
+
|
|
87
|
+
# Python.xcframework is multi-slice and slice-independent, so it is fetched
|
|
88
|
+
# once regardless of how many pip-deps slices we populate. pip-deps itself is
|
|
89
|
+
# installed per slice (device/simulator never share .so files): a bare
|
|
90
|
+
# `kivyforge build` passes both slices so either Xcode destination builds
|
|
91
|
+
# without a re-run, while a targeted build passes only its slice.
|
|
92
|
+
_install_python_xcframework(
|
|
93
|
+
lock, layout, cache=cache, downloader=downloader, no_cache=no_cache
|
|
94
|
+
)
|
|
95
|
+
# Wheel-embedded frameworks are multi-slice xcframeworks; union them across
|
|
96
|
+
# slices (dedup by name) so user-declared natives below don't clobber them.
|
|
97
|
+
# Shared registry of staged frameworks (name -> provenance + content hash).
|
|
98
|
+
# Threaded across every pip-deps slice and into native xcframework
|
|
99
|
+
# extraction so identical duplicates dedupe and conflicting ones fail loudly.
|
|
100
|
+
staged: dict[str, PlacedFramework] = {}
|
|
101
|
+
for build_slice in build_slices:
|
|
102
|
+
slice_pip_deps = layout.pip_deps_for_slice(build_slice.target)
|
|
103
|
+
_install_wheels(
|
|
104
|
+
lock,
|
|
105
|
+
layout,
|
|
106
|
+
slice_pip_deps=slice_pip_deps,
|
|
107
|
+
build_slice=build_slice,
|
|
108
|
+
project_root=project_root,
|
|
109
|
+
cache=cache,
|
|
110
|
+
downloader=downloader,
|
|
111
|
+
no_cache=no_cache,
|
|
112
|
+
runner=runner,
|
|
113
|
+
python_executable=python_executable,
|
|
114
|
+
)
|
|
115
|
+
_compile_pip_deps(
|
|
116
|
+
slice_pip_deps,
|
|
117
|
+
python_version=lock.python_xcframework.version,
|
|
118
|
+
release=release,
|
|
119
|
+
build_settings=build_settings,
|
|
120
|
+
echo=echo,
|
|
121
|
+
)
|
|
122
|
+
_stamp_collected(slice_pip_deps)
|
|
123
|
+
copy_wheel_frameworks(slice_pip_deps, layout.frameworks, existing=staged)
|
|
124
|
+
_install_native_xcframeworks(
|
|
125
|
+
lock,
|
|
126
|
+
layout,
|
|
127
|
+
staged=staged,
|
|
128
|
+
project_root=project_root,
|
|
129
|
+
cache=cache,
|
|
130
|
+
downloader=downloader,
|
|
131
|
+
no_cache=no_cache,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _stamp_collected(slice_pip_deps: Path) -> None:
|
|
136
|
+
"""Mark a pip-deps slice as collected.
|
|
137
|
+
|
|
138
|
+
The Build Python run script uses this marker — not directory emptiness — to
|
|
139
|
+
tell "collected, no third-party deps" (valid: an app with an empty pip-deps
|
|
140
|
+
slice) from "never collected" (error: wrong/uncollected Xcode destination).
|
|
141
|
+
It lives beside the slice dir so it is never rsync'd into the app bundle.
|
|
142
|
+
"""
|
|
143
|
+
marker = slice_pip_deps.parent / f"{slice_pip_deps.name}.collected"
|
|
144
|
+
marker.write_text("", encoding="utf-8")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _setting_applies(value: bool | str, *, release: bool) -> bool:
|
|
148
|
+
"""A ``build_settings`` tri-state resolved for the build being produced."""
|
|
149
|
+
if isinstance(value, bool):
|
|
150
|
+
return value
|
|
151
|
+
return release # DESKTOP_RELEASE_ONLY
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _compile_pip_deps(
|
|
155
|
+
slice_pip_deps: Path,
|
|
156
|
+
*,
|
|
157
|
+
python_version: str,
|
|
158
|
+
release: bool,
|
|
159
|
+
build_settings: DesktopBuildSettings | None,
|
|
160
|
+
echo: Callable[[str], None],
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Byte-compile/strip a collected pip-deps slice, per ``build_settings``.
|
|
163
|
+
|
|
164
|
+
Mirrors each desktop backend's own ``_resolve_byte_compile`` (macOS/Linux/
|
|
165
|
+
Windows ``bundle.py``), with one iOS-specific simplification: the
|
|
166
|
+
Python.xcframework ships no standalone interpreter binary to shell out to
|
|
167
|
+
(it's a linkable library, not an executable), so the "staged interpreter"
|
|
168
|
+
rung of the compiler ladder never applies here — it always falls straight
|
|
169
|
+
to "this process's interpreter, if its CPython minor matches the target",
|
|
170
|
+
or degrades.
|
|
171
|
+
"""
|
|
172
|
+
settings = build_settings or DesktopBuildSettings()
|
|
173
|
+
if not _setting_applies(settings.byte_compile, release=release):
|
|
174
|
+
return
|
|
175
|
+
compiler = select_compiler(
|
|
176
|
+
staged_interpreter=Path(), native=False, python_version=python_version
|
|
177
|
+
)
|
|
178
|
+
if compiler is None:
|
|
179
|
+
message = (
|
|
180
|
+
f"this project ships CPython {python_version}, and no CPython of "
|
|
181
|
+
"that minor could be found to byte-compile with (a .pyc is only "
|
|
182
|
+
"loadable by the exact CPython minor that wrote it)"
|
|
183
|
+
)
|
|
184
|
+
if settings.byte_compile is True:
|
|
185
|
+
raise CollectError(
|
|
186
|
+
"[tool.kivy.ios.python.build_settings].byte_compile = true but "
|
|
187
|
+
f"{message}.\n"
|
|
188
|
+
" Install a matching CPython, or set byte_compile = false."
|
|
189
|
+
)
|
|
190
|
+
echo(f"[stage] not byte-compiling pip-deps: {message}.")
|
|
191
|
+
return
|
|
192
|
+
strip_source = _setting_applies(settings.strip_source, release=release)
|
|
193
|
+
with_what = " ".join(compiler) if compiler else "this interpreter"
|
|
194
|
+
echo(
|
|
195
|
+
f"[stage] byte-compiling pip-deps with {with_what}"
|
|
196
|
+
+ (" (.pyc only)" if strip_source else "")
|
|
197
|
+
)
|
|
198
|
+
try:
|
|
199
|
+
byte_compile(
|
|
200
|
+
[slice_pip_deps],
|
|
201
|
+
compiler=compiler,
|
|
202
|
+
strip_source=strip_source,
|
|
203
|
+
stripdir=slice_pip_deps,
|
|
204
|
+
)
|
|
205
|
+
except PycompileError as exc:
|
|
206
|
+
raise CollectError(
|
|
207
|
+
f"{exc}\n Fix it, or set "
|
|
208
|
+
"[tool.kivy.ios.python.build_settings].byte_compile = false."
|
|
209
|
+
) from exc
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _install_python_xcframework(lock, layout, *, cache, downloader, no_cache) -> None:
|
|
213
|
+
px = lock.python_xcframework
|
|
214
|
+
filename = px.url.rsplit("/", 1)[-1]
|
|
215
|
+
archive = fetch_artifact(
|
|
216
|
+
name=f"Python.xcframework {px.version}",
|
|
217
|
+
sha256=px.sha256,
|
|
218
|
+
filename=filename,
|
|
219
|
+
url=px.url,
|
|
220
|
+
cache=cache,
|
|
221
|
+
downloader=downloader,
|
|
222
|
+
no_cache=no_cache,
|
|
223
|
+
)
|
|
224
|
+
dest = layout.python_xcframework
|
|
225
|
+
if dest.exists():
|
|
226
|
+
shutil.rmtree(dest)
|
|
227
|
+
with tempfile.TemporaryDirectory(prefix="kivy-pyxc-") as tmp:
|
|
228
|
+
tmp_dir = Path(tmp)
|
|
229
|
+
with tarfile.open(archive, "r:gz") as tf:
|
|
230
|
+
# filter="data" rejects path traversal, absolute paths, and
|
|
231
|
+
# unsafe special files (Python 3.12+ / PEP 706).
|
|
232
|
+
tf.extractall(tmp_dir, filter="data")
|
|
233
|
+
found = next(
|
|
234
|
+
(p for p in tmp_dir.rglob("Python.xcframework") if p.is_dir()), None
|
|
235
|
+
)
|
|
236
|
+
if found is None:
|
|
237
|
+
raise CollectError(
|
|
238
|
+
"Python.xcframework not found inside downloaded archive."
|
|
239
|
+
)
|
|
240
|
+
shutil.copytree(found, dest)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _install_wheels(
|
|
244
|
+
lock,
|
|
245
|
+
layout,
|
|
246
|
+
*,
|
|
247
|
+
slice_pip_deps,
|
|
248
|
+
build_slice,
|
|
249
|
+
project_root,
|
|
250
|
+
cache,
|
|
251
|
+
downloader,
|
|
252
|
+
no_cache,
|
|
253
|
+
runner,
|
|
254
|
+
python_executable,
|
|
255
|
+
) -> None:
|
|
256
|
+
with tempfile.TemporaryDirectory(prefix="kivy-wheels-") as tmp:
|
|
257
|
+
stage = Path(tmp)
|
|
258
|
+
wheel_files: list[str] = []
|
|
259
|
+
selected_wheels: list[LockedWheel] = []
|
|
260
|
+
for pkg in lock.packages:
|
|
261
|
+
wheel = select_wheel(pkg, build_slice)
|
|
262
|
+
selected_wheels.append(wheel)
|
|
263
|
+
local = fetch_artifact(
|
|
264
|
+
name=f"{pkg.name} {pkg.version}",
|
|
265
|
+
sha256=wheel.sha256,
|
|
266
|
+
filename=wheel.name,
|
|
267
|
+
url=wheel.url,
|
|
268
|
+
path=wheel.path,
|
|
269
|
+
project_root=project_root,
|
|
270
|
+
cache=cache,
|
|
271
|
+
downloader=downloader,
|
|
272
|
+
no_cache=no_cache,
|
|
273
|
+
)
|
|
274
|
+
# Cache entries are content-addressed ({sha256}-{name}); pip rejects
|
|
275
|
+
# that filename, so stage each wheel under its canonical PEP 427 name.
|
|
276
|
+
staged = stage / wheel.name
|
|
277
|
+
if local.resolve() != staged.resolve():
|
|
278
|
+
shutil.copy2(local, staged)
|
|
279
|
+
wheel_files.append(str(staged))
|
|
280
|
+
if not wheel_files:
|
|
281
|
+
return
|
|
282
|
+
# Each collect always starts from a clean slate so no stale .so
|
|
283
|
+
# files from a previous slice accumulate in the slice-specific dir.
|
|
284
|
+
if slice_pip_deps.is_dir():
|
|
285
|
+
shutil.rmtree(slice_pip_deps)
|
|
286
|
+
slice_pip_deps.mkdir(parents=True, exist_ok=True)
|
|
287
|
+
abi = "cp" + "".join(lock.python_xcframework.version.split(".")[:2])
|
|
288
|
+
# Use the selected wheels' actual platform tag for pip's --platform.
|
|
289
|
+
# The wheel tag reflects the Python binary's compile-time minimum OS
|
|
290
|
+
# (e.g. ios_13_0 from python.org's cp315 binary), which may be lower
|
|
291
|
+
# than the project's deployment_target. pip requires --platform to
|
|
292
|
+
# match the wheel being installed, so use the selected native wheel's
|
|
293
|
+
# tag rather than the computed build_slice tag.
|
|
294
|
+
pip_platform = _native_platform_tag(selected_wheels, build_slice)
|
|
295
|
+
cmd = pip_install_command(
|
|
296
|
+
wheel_files,
|
|
297
|
+
target_dir=str(slice_pip_deps),
|
|
298
|
+
platform_tag=pip_platform,
|
|
299
|
+
python_version=".".join(lock.python_xcframework.version.split(".")[:2]),
|
|
300
|
+
abi=abi,
|
|
301
|
+
python_executable=python_executable,
|
|
302
|
+
)
|
|
303
|
+
proc = runner(cmd, capture_output=True, text=True)
|
|
304
|
+
if proc.returncode != 0:
|
|
305
|
+
raise CollectError(
|
|
306
|
+
f"pip install of pinned wheels failed:\n{proc.stderr or proc.stdout}"
|
|
307
|
+
)
|
|
308
|
+
# pip --target creates a bin/ of host-platform console scripts.
|
|
309
|
+
# Strip it immediately — these macOS executables cannot run on iOS
|
|
310
|
+
# and should not appear in the Xcode project tree or app bundle.
|
|
311
|
+
bin_dir = slice_pip_deps / "bin"
|
|
312
|
+
if bin_dir.is_dir():
|
|
313
|
+
shutil.rmtree(bin_dir)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _install_native_xcframeworks(
|
|
317
|
+
lock, layout, *, staged, project_root, cache, downloader, no_cache
|
|
318
|
+
) -> None:
|
|
319
|
+
for xc in lock.xcframeworks:
|
|
320
|
+
filename = (xc.url or xc.path or xc.name).rsplit("/", 1)[-1]
|
|
321
|
+
archive = fetch_artifact(
|
|
322
|
+
name=xc.name,
|
|
323
|
+
sha256=xc.sha256,
|
|
324
|
+
filename=filename,
|
|
325
|
+
url=xc.url,
|
|
326
|
+
path=xc.path,
|
|
327
|
+
project_root=project_root,
|
|
328
|
+
cache=cache,
|
|
329
|
+
downloader=downloader,
|
|
330
|
+
no_cache=no_cache,
|
|
331
|
+
)
|
|
332
|
+
extract_xcframework_archive(
|
|
333
|
+
archive,
|
|
334
|
+
layout.frameworks,
|
|
335
|
+
name=xc.name,
|
|
336
|
+
archive_format=xc.archive_format,
|
|
337
|
+
archive_member=xc.archive_member,
|
|
338
|
+
existing=staged,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _native_platform_tag(selected: list[LockedWheel], slice_: BuildSlice) -> str:
|
|
343
|
+
"""Return the pip --platform tag to use for the selected wheels.
|
|
344
|
+
|
|
345
|
+
All native wheels in a single build are produced by the same Python binary
|
|
346
|
+
and therefore share the same platform tag (e.g. ios_13_0_arm64_iphoneos).
|
|
347
|
+
Using the wheel's actual tag (rather than the project deployment_target tag)
|
|
348
|
+
ensures pip accepts the wheel even when the wheel's min-OS is lower than the
|
|
349
|
+
project's deployment_target.
|
|
350
|
+
"""
|
|
351
|
+
for wheel in selected:
|
|
352
|
+
if not wheel.is_pure_python:
|
|
353
|
+
return wheel.platform_tag
|
|
354
|
+
return slice_.platform_tag
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Fetch + verify + cache artifacts by URL or repo-relative path (spec 03).
|
|
2
|
+
|
|
3
|
+
A single ``fetch_artifact`` entry point handles the three lockfile sources:
|
|
4
|
+
``url`` (download), local ``path`` (vendored), and cache hits. Every byte that
|
|
5
|
+
enters the build is SHA-256-verified against the lockfile pin before use.
|
|
6
|
+
|
|
7
|
+
The network fetch is injectable (``Downloader``) so unit tests stay hermetic.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import tempfile
|
|
13
|
+
import urllib.request
|
|
14
|
+
from pathlib import Path, PurePosixPath
|
|
15
|
+
from typing import Protocol
|
|
16
|
+
|
|
17
|
+
from ..lock.find_links import FindLinksError, wheel_path_from_project_root
|
|
18
|
+
from .cache import ArtifactCache
|
|
19
|
+
from .verify import HashMismatch, sha256_file, verify_file
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DownloadError(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Downloader(Protocol):
|
|
27
|
+
def fetch_to(self, url: str, dest: Path) -> None: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UrllibDownloader:
|
|
31
|
+
def fetch_to(self, url: str, dest: Path) -> None:
|
|
32
|
+
try:
|
|
33
|
+
with urllib.request.urlopen(url) as resp, open(dest, "wb") as out: # noqa: S310
|
|
34
|
+
for chunk in iter(lambda: resp.read(1 << 20), b""):
|
|
35
|
+
out.write(chunk)
|
|
36
|
+
except (OSError, ValueError) as exc:
|
|
37
|
+
raise DownloadError(f"failed to download {url}: {exc}") from exc
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fetch_artifact(
|
|
41
|
+
*,
|
|
42
|
+
name: str,
|
|
43
|
+
sha256: str,
|
|
44
|
+
filename: str,
|
|
45
|
+
url: str | None = None,
|
|
46
|
+
path: str | None = None,
|
|
47
|
+
project_root: Path | None = None,
|
|
48
|
+
cache: ArtifactCache | None = None,
|
|
49
|
+
downloader: Downloader | None = None,
|
|
50
|
+
no_cache: bool = False,
|
|
51
|
+
) -> Path:
|
|
52
|
+
"""Return a verified local path for an artifact (url xor path).
|
|
53
|
+
|
|
54
|
+
* ``url`` — cache-first download, then SHA-256 verify.
|
|
55
|
+
* ``path`` — repo-relative (resolved against ``project_root``); verified in
|
|
56
|
+
place. Absolute paths or paths escaping the project are rejected.
|
|
57
|
+
"""
|
|
58
|
+
if bool(url) == bool(path):
|
|
59
|
+
raise DownloadError(f"{name}: exactly one of url/path is required")
|
|
60
|
+
|
|
61
|
+
if path is not None:
|
|
62
|
+
return _resolve_local(
|
|
63
|
+
name=name, path=path, sha256=sha256, project_root=project_root
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if url is None:
|
|
67
|
+
# xor check above guarantees one of url/path; path branch already returned.
|
|
68
|
+
raise DownloadError(f"{name}: exactly one of url/path is required")
|
|
69
|
+
|
|
70
|
+
cache = cache or ArtifactCache()
|
|
71
|
+
downloader = downloader or UrllibDownloader()
|
|
72
|
+
|
|
73
|
+
if not no_cache:
|
|
74
|
+
cached = cache.get(sha256, filename)
|
|
75
|
+
if cached is not None and _cache_hit_intact(cached, sha256, name=name):
|
|
76
|
+
return cached
|
|
77
|
+
|
|
78
|
+
with tempfile.TemporaryDirectory(prefix="kivy-dl-") as tmp:
|
|
79
|
+
staged = Path(tmp) / filename
|
|
80
|
+
downloader.fetch_to(url, staged)
|
|
81
|
+
verify_file(staged, sha256, name=name, source=url)
|
|
82
|
+
return cache.put_file(staged, sha256, filename)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _cache_hit_intact(cached: Path, sha256: str, *, name: str) -> bool:
|
|
86
|
+
"""Re-verify a cache hit before trusting it (spec 03 "every byte verified").
|
|
87
|
+
|
|
88
|
+
The cache key is only the *filename* (``{sha256}-{name}``); it is not proof
|
|
89
|
+
that the bytes on disk still match. A non-atomic ``copyfile`` killed mid-write
|
|
90
|
+
leaves a truncated file at the hash-named path, and the cache dir is
|
|
91
|
+
user-writable (bit-rot/tampering). On a mismatch we drop the corrupt entry
|
|
92
|
+
and return False so the caller re-downloads rather than feeding bad bytes
|
|
93
|
+
into the build.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
verify_file(cached, sha256, name=name, source=str(cached))
|
|
97
|
+
except HashMismatch:
|
|
98
|
+
cached.unlink(missing_ok=True)
|
|
99
|
+
return False
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _resolve_local(
|
|
104
|
+
*, name: str, path: str, sha256: str, project_root: Path | None
|
|
105
|
+
) -> Path:
|
|
106
|
+
if PurePosixPath(path).is_absolute() or Path(path).is_absolute():
|
|
107
|
+
raise DownloadError(
|
|
108
|
+
f"{name}: lockfile path {path!r} is absolute; only repo-relative "
|
|
109
|
+
f"paths are allowed (they resolve identically on every clone)."
|
|
110
|
+
)
|
|
111
|
+
root = (project_root or Path.cwd()).resolve()
|
|
112
|
+
resolved = (root / path).resolve()
|
|
113
|
+
try:
|
|
114
|
+
wheel_path_from_project_root(root, resolved)
|
|
115
|
+
except FindLinksError as exc:
|
|
116
|
+
raise DownloadError(
|
|
117
|
+
f"{name}: lockfile path {path!r} escapes the allowed vendored "
|
|
118
|
+
f"wheel scope for this project."
|
|
119
|
+
) from exc
|
|
120
|
+
if not resolved.is_file():
|
|
121
|
+
raise DownloadError(f"{name}: vendored artifact not found at {path!r}.")
|
|
122
|
+
actual = sha256_file(resolved)
|
|
123
|
+
if actual != sha256:
|
|
124
|
+
raise HashMismatch(name=name, source=path, expected=sha256, actual=actual)
|
|
125
|
+
return resolved
|