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,280 @@
|
|
|
1
|
+
"""Assemble a ``WheelRuntimeLock`` from a ``Config`` + a platform profile.
|
|
2
|
+
|
|
3
|
+
Generic orchestration shared by every wheel+runtime platform: validate/resolve
|
|
4
|
+
find_links, pin the runtime, resolve wheels per variant, fail-fast on any variant
|
|
5
|
+
a compiled dependency does not cover, and assemble the lock. The only
|
|
6
|
+
platform-specific inputs come from the ``PlatformLockProfile``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from urllib.parse import unquote, urlparse
|
|
16
|
+
|
|
17
|
+
from ... import __version__
|
|
18
|
+
from ...artifacts.verify import sha256_file
|
|
19
|
+
from ...config.model import Config, NativeBinaryDep
|
|
20
|
+
from ..find_links import (
|
|
21
|
+
FindLinksError,
|
|
22
|
+
find_links_resolution_hint,
|
|
23
|
+
resolve_find_links,
|
|
24
|
+
validate_find_links,
|
|
25
|
+
wheel_path_from_project_root,
|
|
26
|
+
)
|
|
27
|
+
from ..model import LockedPackage, LockedWheel, PackageDep, canonical_name
|
|
28
|
+
from ..reader import compute_pyproject_sha256
|
|
29
|
+
from .model import WheelRuntimeLock
|
|
30
|
+
from .native_binaries import NativeBinaryResolverError, resolve_native_binaries
|
|
31
|
+
from .profile import PlatformLockProfile
|
|
32
|
+
from .resolver import WheelResolver, WheelResolverError
|
|
33
|
+
from .runtime import RuntimeProvider, RuntimeProviderError
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class WheelRuntimeBuildError(Exception):
|
|
37
|
+
"""A wheel+runtime lock build failure surfaced with an actionable message."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_wheel_runtime_lock(
|
|
41
|
+
profile: PlatformLockProfile,
|
|
42
|
+
config: Config,
|
|
43
|
+
pyproject_text: str,
|
|
44
|
+
*,
|
|
45
|
+
project_root: Path | None = None,
|
|
46
|
+
resolver: WheelResolver | None = None,
|
|
47
|
+
runtime_provider: RuntimeProvider | None = None,
|
|
48
|
+
native_binaries: tuple[NativeBinaryDep, ...] = (),
|
|
49
|
+
offline: bool = False,
|
|
50
|
+
now: datetime | None = None,
|
|
51
|
+
on_warning: Callable[[str], None] | None = None,
|
|
52
|
+
) -> WheelRuntimeLock:
|
|
53
|
+
if profile.overlay(config) is None:
|
|
54
|
+
raise WheelRuntimeBuildError(profile.missing_overlay_error())
|
|
55
|
+
|
|
56
|
+
resolver = resolver or profile.wheel_resolver()
|
|
57
|
+
runtime_provider = runtime_provider or profile.runtime_provider(config)
|
|
58
|
+
root = (project_root or Path.cwd()).resolve()
|
|
59
|
+
|
|
60
|
+
python_version = profile.python_version(config)
|
|
61
|
+
variants = profile.variants(config)
|
|
62
|
+
archs = profile.archs(config)
|
|
63
|
+
|
|
64
|
+
find_links_entries = profile.find_links(config)
|
|
65
|
+
try:
|
|
66
|
+
validate_find_links(root, find_links_entries, platform=profile.platform)
|
|
67
|
+
except FindLinksError as exc:
|
|
68
|
+
raise WheelRuntimeBuildError(str(exc)) from exc
|
|
69
|
+
find_links = resolve_find_links(root, find_links_entries)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
runtime = runtime_provider.resolve(python_version, archs, offline=offline)
|
|
73
|
+
except RuntimeProviderError as exc:
|
|
74
|
+
raise WheelRuntimeBuildError(str(exc)) from exc
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
locked_native_binaries = resolve_native_binaries(
|
|
78
|
+
native_binaries, project_root=root, offline=offline
|
|
79
|
+
)
|
|
80
|
+
except NativeBinaryResolverError as exc:
|
|
81
|
+
raise WheelRuntimeBuildError(str(exc)) from exc
|
|
82
|
+
|
|
83
|
+
declared_floor = profile.declared_floor(config)
|
|
84
|
+
if (
|
|
85
|
+
declared_floor
|
|
86
|
+
and runtime.floor
|
|
87
|
+
and _version_tuple(declared_floor) < _version_tuple(runtime.floor)
|
|
88
|
+
):
|
|
89
|
+
raise WheelRuntimeBuildError(
|
|
90
|
+
profile.floor_error(declared_floor, runtime.floor, python_version)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
direct = {canonical_name(_req_name(d)) for d in config.project.dependencies}
|
|
94
|
+
excluded = {canonical_name(e) for e in profile.exclude(config)} - direct
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
resolved = resolver.resolve(
|
|
98
|
+
list(config.project.dependencies),
|
|
99
|
+
python_version=python_version,
|
|
100
|
+
variants=variants,
|
|
101
|
+
extra_index_urls=list(profile.extra_index_urls(config)),
|
|
102
|
+
find_links=find_links,
|
|
103
|
+
offline=offline,
|
|
104
|
+
)
|
|
105
|
+
except WheelResolverError as exc:
|
|
106
|
+
hint = find_links_resolution_hint(
|
|
107
|
+
root, find_links_entries, pip_stderr=str(exc), platform=profile.platform
|
|
108
|
+
)
|
|
109
|
+
if hint:
|
|
110
|
+
raise WheelRuntimeBuildError(f"{exc}\n{hint}") from exc
|
|
111
|
+
raise WheelRuntimeBuildError(str(exc)) from exc
|
|
112
|
+
|
|
113
|
+
packages = []
|
|
114
|
+
for rp in resolved:
|
|
115
|
+
if canonical_name(rp.name) in excluded:
|
|
116
|
+
continue
|
|
117
|
+
wheels = tuple(
|
|
118
|
+
_locked_wheel_from_resolved(profile, w, project_root=root)
|
|
119
|
+
for w in rp.wheels
|
|
120
|
+
)
|
|
121
|
+
_check_variants_complete(profile, rp.name, wheels, archs, on_warning=on_warning)
|
|
122
|
+
packages.append(
|
|
123
|
+
LockedPackage(
|
|
124
|
+
name=rp.name,
|
|
125
|
+
version=rp.version,
|
|
126
|
+
wheels=wheels,
|
|
127
|
+
requires_python=rp.requires_python,
|
|
128
|
+
dependencies=tuple(PackageDep(name=d) for d in rp.dependencies),
|
|
129
|
+
direct_requirement=canonical_name(rp.name) in direct,
|
|
130
|
+
source_index=rp.source_index,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return WheelRuntimeLock(
|
|
135
|
+
platform=profile.platform,
|
|
136
|
+
requires_python=config.project.requires_python or ">=3.15",
|
|
137
|
+
# Canonical (serializer) order so a freshly built lock compares equal to
|
|
138
|
+
# its round-tripped form; ``lock --check`` re-resolves and compares
|
|
139
|
+
# order-sensitively against the on-disk (sorted) lock.
|
|
140
|
+
packages=tuple(sorted(packages, key=lambda p: p.sort_key)),
|
|
141
|
+
python_runtime=runtime,
|
|
142
|
+
archs=archs,
|
|
143
|
+
kivyforge_version=__version__,
|
|
144
|
+
generated_at=(now or datetime.now(UTC)).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
145
|
+
pyproject_sha256=compute_pyproject_sha256(pyproject_text),
|
|
146
|
+
tool_kivyforge_schema_version=profile.schema_version(config),
|
|
147
|
+
native_binaries=locked_native_binaries,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _locked_wheel_from_resolved(
|
|
152
|
+
profile: PlatformLockProfile, w, *, project_root: Path
|
|
153
|
+
) -> LockedWheel:
|
|
154
|
+
url, path = _normalize_wheel_source(profile, w.url, project_root=project_root)
|
|
155
|
+
sha256 = w.sha256
|
|
156
|
+
if not sha256 and path:
|
|
157
|
+
sha256 = sha256_file((project_root / path).resolve())
|
|
158
|
+
if not sha256:
|
|
159
|
+
raise WheelRuntimeBuildError(
|
|
160
|
+
f"could not determine SHA-256 for wheel {w.filename!r}; "
|
|
161
|
+
f"re-run lock with network access or check the vendored file."
|
|
162
|
+
)
|
|
163
|
+
return LockedWheel(
|
|
164
|
+
name=w.filename,
|
|
165
|
+
url=url,
|
|
166
|
+
path=path,
|
|
167
|
+
sha256=sha256,
|
|
168
|
+
upload_time=w.upload_time,
|
|
169
|
+
size=w.size,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _normalize_wheel_source(
|
|
174
|
+
profile: PlatformLockProfile, url: str, *, project_root: Path
|
|
175
|
+
) -> tuple[str | None, str | None]:
|
|
176
|
+
if url.startswith(("http://", "https://")):
|
|
177
|
+
return url, None
|
|
178
|
+
if url.startswith("file:"):
|
|
179
|
+
local = Path(unquote(urlparse(url).path))
|
|
180
|
+
else:
|
|
181
|
+
local = Path(url)
|
|
182
|
+
resolved = local.resolve()
|
|
183
|
+
root = project_root.resolve()
|
|
184
|
+
try:
|
|
185
|
+
rel = wheel_path_from_project_root(root, resolved)
|
|
186
|
+
except FindLinksError as exc:
|
|
187
|
+
raise WheelRuntimeBuildError(
|
|
188
|
+
f"wheel resolved to {resolved}, which is outside the allowed "
|
|
189
|
+
f"find_links scope for project directory {root}.\n"
|
|
190
|
+
f" {profile.wheel_scope_hint()}"
|
|
191
|
+
) from exc
|
|
192
|
+
return None, rel
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _check_variants_complete(
|
|
196
|
+
profile: PlatformLockProfile,
|
|
197
|
+
name: str,
|
|
198
|
+
wheels: tuple[LockedWheel, ...],
|
|
199
|
+
archs: tuple[str, ...],
|
|
200
|
+
*,
|
|
201
|
+
on_warning: Callable[[str], None] | None = None,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Fail fast if a compiled package is missing a required variant.
|
|
204
|
+
|
|
205
|
+
Pure-Python packages (a single ``py3-none-any`` wheel) are always complete.
|
|
206
|
+
Coverage is source-aware: a platform may decline a wheel (and/or attach a
|
|
207
|
+
non-fatal warning) based on where it came from — see
|
|
208
|
+
:meth:`PlatformLockProfile.wheel_coverage`.
|
|
209
|
+
"""
|
|
210
|
+
if any(w.is_pure_python for w in wheels):
|
|
211
|
+
return
|
|
212
|
+
covered: set[str] = set()
|
|
213
|
+
for wheel in wheels:
|
|
214
|
+
wheel_archs, warning = profile.wheel_coverage(wheel, archs)
|
|
215
|
+
covered.update(wheel_archs)
|
|
216
|
+
if warning and on_warning is not None:
|
|
217
|
+
on_warning(warning)
|
|
218
|
+
missing = [a for a in archs if a not in covered]
|
|
219
|
+
if missing:
|
|
220
|
+
raise WheelRuntimeBuildError(profile.coverage_error(name, missing))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def semantic_equal(a: WheelRuntimeLock, b: WheelRuntimeLock) -> bool:
|
|
224
|
+
"""Compare two locks ignoring the volatile ``generated_at`` field."""
|
|
225
|
+
return dataclasses.replace(a, generated_at="") == dataclasses.replace(
|
|
226
|
+
b, generated_at=""
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def diff_summary(old: WheelRuntimeLock, new: WheelRuntimeLock) -> list[str]:
|
|
231
|
+
out: list[str] = []
|
|
232
|
+
old_pkgs = {canonical_name(p.name): p for p in old.packages}
|
|
233
|
+
new_pkgs = {canonical_name(p.name): p for p in new.packages}
|
|
234
|
+
for name in sorted(new_pkgs.keys() - old_pkgs.keys()):
|
|
235
|
+
out.append(f" + {new_pkgs[name].name} {new_pkgs[name].version} (added)")
|
|
236
|
+
for name in sorted(old_pkgs.keys() - new_pkgs.keys()):
|
|
237
|
+
out.append(f" - {old_pkgs[name].name} {old_pkgs[name].version} (removed)")
|
|
238
|
+
for name in sorted(old_pkgs.keys() & new_pkgs.keys()):
|
|
239
|
+
if old_pkgs[name].version != new_pkgs[name].version:
|
|
240
|
+
out.append(
|
|
241
|
+
f" ~ {new_pkgs[name].name}: "
|
|
242
|
+
f"{old_pkgs[name].version} -> {new_pkgs[name].version}"
|
|
243
|
+
)
|
|
244
|
+
if old.python_runtime.version != new.python_runtime.version:
|
|
245
|
+
out.append(
|
|
246
|
+
f" ~ python runtime: {old.python_runtime.version} -> "
|
|
247
|
+
f"{new.python_runtime.version}"
|
|
248
|
+
)
|
|
249
|
+
old_nb = {b.name: b for b in old.native_binaries}
|
|
250
|
+
new_nb = {b.name: b for b in new.native_binaries}
|
|
251
|
+
for name in sorted(new_nb.keys() - old_nb.keys()):
|
|
252
|
+
out.append(
|
|
253
|
+
f" + native binary {new_nb[name].name} {new_nb[name].version} (added)"
|
|
254
|
+
)
|
|
255
|
+
for name in sorted(old_nb.keys() - new_nb.keys()):
|
|
256
|
+
out.append(
|
|
257
|
+
f" - native binary {old_nb[name].name} {old_nb[name].version} (removed)"
|
|
258
|
+
)
|
|
259
|
+
for name in sorted(old_nb.keys() & new_nb.keys()):
|
|
260
|
+
o, n = old_nb[name], new_nb[name]
|
|
261
|
+
if o.version != n.version or o.sha256 != n.sha256:
|
|
262
|
+
out.append(f" ~ native binary {n.name}: {o.version} -> {n.version}")
|
|
263
|
+
return out
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _req_name(requirement: str) -> str:
|
|
267
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
return Requirement(requirement).name
|
|
271
|
+
except InvalidRequirement:
|
|
272
|
+
return requirement
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _version_tuple(v: str) -> tuple[int, ...]:
|
|
276
|
+
parts = []
|
|
277
|
+
for chunk in v.split("."):
|
|
278
|
+
digits = "".join(c for c in chunk if c.isdigit())
|
|
279
|
+
parts.append(int(digits) if digits else 0)
|
|
280
|
+
return tuple(parts)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Generic ``wheel + bundled-runtime`` lockfile model.
|
|
2
|
+
|
|
3
|
+
Shared by every platform whose lock is "platform-tagged wheels plus a bundled
|
|
4
|
+
CPython runtime": macOS today, and Linux/Windows/Android in later phases. Each
|
|
5
|
+
such platform differs only in a small *profile* (tag construction, the arch/ABI
|
|
6
|
+
variant set, and the runtime provider); the lockfile *shape* is identical, so it
|
|
7
|
+
lives here once. iOS is deliberately not part of this family — its per-slice
|
|
8
|
+
framework conversion, ``Python.xcframework`` runtime, and SPM extension give it a
|
|
9
|
+
genuinely different lock shape.
|
|
10
|
+
|
|
11
|
+
PEP 751 ``[[packages]]`` (the generic ``LockedPackage``/``LockedWheel`` from
|
|
12
|
+
``kivyforge.lock.model``) plus a ``[tool.kivyforge]`` extension recording the
|
|
13
|
+
arch set and the runtime pin (provider + one archive per arch).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from ..model import LOCK_VERSION, LockedPackage
|
|
21
|
+
|
|
22
|
+
# The wheel-runtime [tool.kivyforge] extension schema version.
|
|
23
|
+
TOOL_SCHEMA_VERSION = 1
|
|
24
|
+
CREATED_BY = "kivyforge"
|
|
25
|
+
DEFAULT_REQUIRES_PYTHON = ">=3.15"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class RuntimeArtifact:
|
|
30
|
+
"""One per-arch bundled Python runtime archive (pinned by URL + SHA-256).
|
|
31
|
+
|
|
32
|
+
``arch`` is the variant identifier: a CPU architecture on desktops
|
|
33
|
+
(``arm64``/``x86_64``) or an ABI on Android (``arm64-v8a``/…).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
arch: str
|
|
37
|
+
url: str
|
|
38
|
+
sha256: str
|
|
39
|
+
archive_format: str = "tar.gz"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class PythonRuntime:
|
|
44
|
+
"""The bundled CPython runtime pin (provider-agnostic)."""
|
|
45
|
+
|
|
46
|
+
provider: str # e.g. "python-build-standalone"
|
|
47
|
+
version: str # CPython version, e.g. "3.15.0"
|
|
48
|
+
artifacts: tuple[RuntimeArtifact, ...] # one per arch in the lock
|
|
49
|
+
floor: str | None = None # runtime's own OS floor, if the provider reports it
|
|
50
|
+
|
|
51
|
+
def artifact_for(self, arch: str) -> RuntimeArtifact | None:
|
|
52
|
+
for art in self.artifacts:
|
|
53
|
+
if art.arch == arch:
|
|
54
|
+
return art
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class LockedNativeBinary:
|
|
60
|
+
"""One pinned ``[tool.kivy.<platform>.native.binaries]`` entry.
|
|
61
|
+
|
|
62
|
+
A non-wheel native binary (vendor SDK dylib/so/DLL or a helper executable)
|
|
63
|
+
pinned by SHA-256. Exactly one of ``url`` (remote source) or ``path``
|
|
64
|
+
(repo-relative vendored source) is set — the ``LockedXcframework``
|
|
65
|
+
convention.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
name: str
|
|
69
|
+
version: str
|
|
70
|
+
sha256: str
|
|
71
|
+
url: str | None = None
|
|
72
|
+
path: str | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class WheelRuntimeLock:
|
|
77
|
+
"""A ``pylock.<platform>.toml`` for the wheel + bundled-runtime family."""
|
|
78
|
+
|
|
79
|
+
platform: str # "macos" | "linux" | "windows" | "android"
|
|
80
|
+
requires_python: str
|
|
81
|
+
packages: tuple[LockedPackage, ...]
|
|
82
|
+
python_runtime: PythonRuntime
|
|
83
|
+
archs: tuple[str, ...]
|
|
84
|
+
kivyforge_version: str
|
|
85
|
+
generated_at: str
|
|
86
|
+
pyproject_sha256: str
|
|
87
|
+
tool_kivyforge_schema_version: int
|
|
88
|
+
schema_version: int = TOOL_SCHEMA_VERSION
|
|
89
|
+
lock_version: str = LOCK_VERSION
|
|
90
|
+
created_by: str = CREATED_BY
|
|
91
|
+
extras: tuple[str, ...] = ()
|
|
92
|
+
dependency_groups: tuple[str, ...] = ()
|
|
93
|
+
default_groups: tuple[str, ...] = ()
|
|
94
|
+
native_binaries: tuple[LockedNativeBinary, ...] = ()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Native-binary resolver (the ``native.binaries`` channel, macos-spec).
|
|
2
|
+
|
|
3
|
+
``kivyforge lock`` reads each declared ``[tool.kivy.<platform>.native.binaries]``
|
|
4
|
+
entry's ``source`` (a single native binary or a ``.zip`` of several, referenced
|
|
5
|
+
by URL or repo-relative path) and computes its SHA-256. Those pins land in
|
|
6
|
+
``pylock.<platform>.toml`` so ``kivyforge build`` can re-fetch the artifact by
|
|
7
|
+
``url``+``sha256`` (or ``path``) and stage it into the bundle's ``bin`` directory
|
|
8
|
+
reproducibly.
|
|
9
|
+
|
|
10
|
+
The download backend is injectable (``Downloader``) so unit tests stay hermetic
|
|
11
|
+
(no network). This mirrors ``platforms/ios/lock/xcframework.py`` without the
|
|
12
|
+
per-slice framework enumeration — a native binary has no slices to read.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from kivyforge.artifacts.verify import sha256_file
|
|
22
|
+
from kivyforge.config.model import NativeBinaryDep
|
|
23
|
+
from kivyforge.lock.find_links import FindLinksError, wheel_path_from_project_root
|
|
24
|
+
|
|
25
|
+
from .model import LockedNativeBinary
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from kivyforge.artifacts.cache import ArtifactCache
|
|
29
|
+
from kivyforge.artifacts.download import Downloader
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NativeBinaryResolverError(Exception):
|
|
33
|
+
"""A native-binary resolution failure surfaced with an actionable message."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_native_binaries(
|
|
37
|
+
declared: tuple[NativeBinaryDep, ...],
|
|
38
|
+
*,
|
|
39
|
+
project_root: Path,
|
|
40
|
+
downloader: Downloader | None = None,
|
|
41
|
+
cache: ArtifactCache | None = None,
|
|
42
|
+
offline: bool = False,
|
|
43
|
+
) -> tuple[LockedNativeBinary, ...]:
|
|
44
|
+
"""Resolve declared native binaries into SHA-256-pinned lock entries.
|
|
45
|
+
|
|
46
|
+
Returns entries sorted by ``(name, version)``. The downloader is only invoked
|
|
47
|
+
for remote (URL) sources, so a project with only vendored binaries (or none)
|
|
48
|
+
never needs network access. ``cache`` is accepted for call-site symmetry with
|
|
49
|
+
the build-time fetch path; lock resolution computes the pin from scratch.
|
|
50
|
+
"""
|
|
51
|
+
if not declared:
|
|
52
|
+
return ()
|
|
53
|
+
out = [
|
|
54
|
+
_resolve_one(
|
|
55
|
+
dep, project_root=project_root, downloader=downloader, offline=offline
|
|
56
|
+
)
|
|
57
|
+
for dep in declared
|
|
58
|
+
]
|
|
59
|
+
out.sort(key=lambda x: (x.name.lower(), x.version))
|
|
60
|
+
return tuple(out)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _resolve_one(
|
|
64
|
+
dep: NativeBinaryDep,
|
|
65
|
+
*,
|
|
66
|
+
project_root: Path,
|
|
67
|
+
downloader: Downloader | None,
|
|
68
|
+
offline: bool,
|
|
69
|
+
) -> LockedNativeBinary:
|
|
70
|
+
is_url = dep.source.startswith(("http://", "https://"))
|
|
71
|
+
if is_url:
|
|
72
|
+
with tempfile.TemporaryDirectory(prefix="kivy-nb-lock-") as tmp:
|
|
73
|
+
archive = _download(dep, Path(tmp), downloader=downloader, offline=offline)
|
|
74
|
+
sha256 = sha256_file(archive)
|
|
75
|
+
return LockedNativeBinary(
|
|
76
|
+
name=dep.name, version=dep.version, sha256=sha256, url=dep.source
|
|
77
|
+
)
|
|
78
|
+
resolved, rel = _resolve_local(dep, project_root=project_root)
|
|
79
|
+
return LockedNativeBinary(
|
|
80
|
+
name=dep.name, version=dep.version, sha256=sha256_file(resolved), path=rel
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _download(
|
|
85
|
+
dep: NativeBinaryDep,
|
|
86
|
+
tmpdir: Path,
|
|
87
|
+
*,
|
|
88
|
+
downloader: Downloader | None,
|
|
89
|
+
offline: bool,
|
|
90
|
+
) -> Path:
|
|
91
|
+
# Imported lazily: artifacts.download imports lock.find_links, so a top-level
|
|
92
|
+
# import here would close an import cycle (lock <-> artifacts).
|
|
93
|
+
from kivyforge.artifacts.download import DownloadError, UrllibDownloader
|
|
94
|
+
|
|
95
|
+
if offline:
|
|
96
|
+
raise NativeBinaryResolverError(
|
|
97
|
+
f"native binary {dep.name!r}: cannot download {dep.source} while "
|
|
98
|
+
f"offline.\n Re-run `kivyforge lock` with network access, or vendor "
|
|
99
|
+
f"the file and point `source` at a repo-relative path."
|
|
100
|
+
)
|
|
101
|
+
dl = downloader or UrllibDownloader()
|
|
102
|
+
filename = dep.source.rsplit("/", 1)[-1] or dep.name
|
|
103
|
+
dest = tmpdir / filename
|
|
104
|
+
try:
|
|
105
|
+
dl.fetch_to(dep.source, dest)
|
|
106
|
+
except DownloadError as exc:
|
|
107
|
+
raise NativeBinaryResolverError(f"native binary {dep.name!r}: {exc}") from exc
|
|
108
|
+
return dest
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolve_local(dep: NativeBinaryDep, *, project_root: Path) -> tuple[Path, str]:
|
|
112
|
+
root = project_root.resolve()
|
|
113
|
+
resolved = (root / dep.source).resolve()
|
|
114
|
+
try:
|
|
115
|
+
rel = wheel_path_from_project_root(root, resolved)
|
|
116
|
+
except FindLinksError as exc:
|
|
117
|
+
raise NativeBinaryResolverError(
|
|
118
|
+
f"native binary {dep.name!r}: source {dep.source!r} resolves to "
|
|
119
|
+
f"{resolved}, which is outside the project directory and its parent.\n"
|
|
120
|
+
f" Vendored native binaries must live under the project directory "
|
|
121
|
+
f"or a shared sibling directory (e.g. examples/binaries/)."
|
|
122
|
+
) from exc
|
|
123
|
+
if not resolved.is_file():
|
|
124
|
+
raise NativeBinaryResolverError(
|
|
125
|
+
f"native binary {dep.name!r}: vendored file not found at {dep.source!r}."
|
|
126
|
+
)
|
|
127
|
+
return resolved, rel
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""GitHub-backed python-build-standalone (PBS) release metadata lookup.
|
|
2
|
+
|
|
3
|
+
PBS publishes builds as GitHub release assets on
|
|
4
|
+
``astral-sh/python-build-standalone``. Each release is tagged by date and ships,
|
|
5
|
+
per platform, an ``install_only`` archive plus a companion ``.sha256`` file. This
|
|
6
|
+
fetcher finds the newest release providing the requested version + target triple
|
|
7
|
+
and returns its download URL + pinned SHA-256. It is triple-agnostic, so macOS
|
|
8
|
+
and (later) Linux share it unchanged.
|
|
9
|
+
|
|
10
|
+
This is the only networked part of the runtime provider; it is exercised against
|
|
11
|
+
the live API at the phase stop rather than in hermetic unit tests (which inject a
|
|
12
|
+
fake ``MetadataFetcher``).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import urllib.request
|
|
20
|
+
|
|
21
|
+
from .runtime import ReleaseAsset, RuntimeProviderError
|
|
22
|
+
|
|
23
|
+
_API = "https://api.github.com/repos/astral-sh/python-build-standalone"
|
|
24
|
+
# Each PBS release bundles *every* supported version × platform (hundreds of
|
|
25
|
+
# assets), so listing many full release objects at once makes the API 504. The
|
|
26
|
+
# newest release already contains every actively-supported CPython, so we check
|
|
27
|
+
# `releases/latest` first and only fall back to small paged scans for an older
|
|
28
|
+
# pin. Keep the page size small for the same 504-avoidance reason.
|
|
29
|
+
_LATEST_URL = f"{_API}/releases/latest"
|
|
30
|
+
_RELEASES_URL = f"{_API}/releases?per_page={{per_page}}&page={{page}}"
|
|
31
|
+
_PAGE_SIZE = 10
|
|
32
|
+
_MAX_PAGES = 15
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GithubPbsMetadataFetcher:
|
|
36
|
+
"""Resolve a PBS asset by querying the GitHub releases API."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, latest_url: str = _LATEST_URL) -> None:
|
|
39
|
+
self._latest_url = latest_url
|
|
40
|
+
|
|
41
|
+
def fetch(
|
|
42
|
+
self, version: str, target_triple: str, *, offline: bool = False
|
|
43
|
+
) -> ReleaseAsset:
|
|
44
|
+
if offline:
|
|
45
|
+
raise RuntimeProviderError(
|
|
46
|
+
"cannot resolve a python-build-standalone runtime offline; "
|
|
47
|
+
"re-run `kivyforge lock` with network access."
|
|
48
|
+
)
|
|
49
|
+
pattern = re.compile(
|
|
50
|
+
rf"^cpython-{re.escape(version)}\+\d+-{re.escape(target_triple)}"
|
|
51
|
+
r"-install_only\.tar\.gz$"
|
|
52
|
+
)
|
|
53
|
+
for release in self._candidate_releases():
|
|
54
|
+
asset = self._match_release(release, pattern)
|
|
55
|
+
if asset is not None:
|
|
56
|
+
return asset
|
|
57
|
+
raise RuntimeProviderError(
|
|
58
|
+
f"no python-build-standalone install_only build found for CPython "
|
|
59
|
+
f"{version} ({target_triple}).\n"
|
|
60
|
+
f" Check available versions at "
|
|
61
|
+
f"https://github.com/astral-sh/python-build-standalone/releases."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _candidate_releases(self):
|
|
65
|
+
"""Yield the latest release, then older releases page by page."""
|
|
66
|
+
yield self._get_json(self._latest_url)
|
|
67
|
+
for page in range(1, _MAX_PAGES + 1):
|
|
68
|
+
url = _RELEASES_URL.format(per_page=_PAGE_SIZE, page=page)
|
|
69
|
+
releases = self._get_json(url)
|
|
70
|
+
if not releases:
|
|
71
|
+
return
|
|
72
|
+
yield from releases
|
|
73
|
+
|
|
74
|
+
def _match_release(self, release, pattern) -> ReleaseAsset | None:
|
|
75
|
+
assets = {a["name"]: a for a in release.get("assets", [])}
|
|
76
|
+
match = next((n for n in assets if pattern.match(n)), None)
|
|
77
|
+
if match is None:
|
|
78
|
+
return None
|
|
79
|
+
url = assets[match]["browser_download_url"]
|
|
80
|
+
sha256 = self._read_sha256(assets, match, url)
|
|
81
|
+
return ReleaseAsset(url=url, sha256=sha256)
|
|
82
|
+
|
|
83
|
+
def _read_sha256(self, assets: dict, name: str, url: str) -> str:
|
|
84
|
+
"""Pin the asset's SHA-256, tried most→least authoritative.
|
|
85
|
+
|
|
86
|
+
1. GitHub's asset ``digest`` field (``sha256:<hex>``) — no extra fetch.
|
|
87
|
+
2. A per-asset ``<name>.sha256`` companion.
|
|
88
|
+
3. The release-wide ``SHA256SUMS`` manifest.
|
|
89
|
+
"""
|
|
90
|
+
digest = assets[name].get("digest") or ""
|
|
91
|
+
if digest.startswith("sha256:"):
|
|
92
|
+
return digest.split(":", 1)[1].strip()
|
|
93
|
+
|
|
94
|
+
companion = f"{name}.sha256"
|
|
95
|
+
if companion in assets:
|
|
96
|
+
text = self._get_text(assets[companion]["browser_download_url"])
|
|
97
|
+
return text.split()[0].strip()
|
|
98
|
+
|
|
99
|
+
if "SHA256SUMS" in assets:
|
|
100
|
+
sha = self._sha_from_manifest(
|
|
101
|
+
assets["SHA256SUMS"]["browser_download_url"], name
|
|
102
|
+
)
|
|
103
|
+
if sha is not None:
|
|
104
|
+
return sha
|
|
105
|
+
|
|
106
|
+
raise RuntimeProviderError(
|
|
107
|
+
f"python-build-standalone asset {name!r} exposes no SHA-256 (no "
|
|
108
|
+
f"digest, .sha256, or SHA256SUMS entry); cannot pin it "
|
|
109
|
+
f"reproducibly ({url})."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _sha_from_manifest(self, url: str, name: str) -> str | None:
|
|
113
|
+
for line in self._get_text(url).splitlines():
|
|
114
|
+
parts = line.split()
|
|
115
|
+
# "<hex> <filename>" — match the filename column exactly.
|
|
116
|
+
if len(parts) == 2 and parts[1].strip() == name:
|
|
117
|
+
return parts[0].strip()
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def _get_json(self, url: str):
|
|
121
|
+
return json.loads(self._get_text(url))
|
|
122
|
+
|
|
123
|
+
def _get_text(self, url: str) -> str:
|
|
124
|
+
req = urllib.request.Request( # noqa: S310 — https GitHub API only
|
|
125
|
+
url, headers={"Accept": "application/vnd.github+json"}
|
|
126
|
+
)
|
|
127
|
+
try:
|
|
128
|
+
with urllib.request.urlopen(req) as resp: # noqa: S310
|
|
129
|
+
return resp.read().decode("utf-8")
|
|
130
|
+
except (OSError, ValueError) as exc:
|
|
131
|
+
raise RuntimeProviderError(
|
|
132
|
+
f"failed to query python-build-standalone metadata ({url}): {exc}"
|
|
133
|
+
) from exc
|