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,104 @@
|
|
|
1
|
+
"""The per-platform lock *profile* for the wheel+runtime family.
|
|
2
|
+
|
|
3
|
+
A profile is the small, platform-specific seam the generic builder/resolver
|
|
4
|
+
depend on. It reads the platform's ``[tool.kivy.<platform>]`` overlay off the
|
|
5
|
+
shared ``Config`` and answers: which variants to resolve, how a wheel tag maps to
|
|
6
|
+
variants (the coverage rule), which runtime provider to use, and the
|
|
7
|
+
platform-appropriate error/hint wording. Adding Linux/Windows/Android is writing
|
|
8
|
+
one of these — no new builder, resolver, model, or serializer.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
|
|
15
|
+
from ...config.model import Config
|
|
16
|
+
from .resolver import Variant, WheelResolver, get_wheel_resolver
|
|
17
|
+
from .runtime import RuntimeProvider
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PlatformLockProfile(ABC):
|
|
21
|
+
"""Platform-specific inputs to the generic wheel+runtime lock engine."""
|
|
22
|
+
|
|
23
|
+
#: Platform name; the ``pylock.<platform>.toml`` infix + overlay key.
|
|
24
|
+
platform: str
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def overlay(self, config: Config):
|
|
28
|
+
"""The platform overlay object (e.g. ``config.macos``), or ``None``."""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def missing_overlay_error(self) -> str:
|
|
32
|
+
"""Message when the overlay is absent (nothing to lock)."""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def python_version(self, config: Config) -> str:
|
|
36
|
+
"""The bundled CPython version to pin."""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def variants(self, config: Config) -> tuple[Variant, ...]:
|
|
40
|
+
"""The build variants to resolve (arch/ABI + pip ``--platform`` tag)."""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def wheel_covers(self, platform_tag: str, archs: tuple[str, ...]) -> set[str]:
|
|
44
|
+
"""The subset of ``archs`` a wheel's platform tag satisfies.
|
|
45
|
+
|
|
46
|
+
A fat wheel (e.g. macOS ``universal2``) returns every arch; a per-arch
|
|
47
|
+
wheel returns its own; an unrelated tag returns the empty set.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def wheel_coverage(
|
|
51
|
+
self, wheel, archs: tuple[str, ...]
|
|
52
|
+
) -> tuple[set[str], str | None]:
|
|
53
|
+
"""Archs a resolved *wheel* covers, plus an optional non-fatal warning.
|
|
54
|
+
|
|
55
|
+
Default: the pure tag→arch rule (:meth:`wheel_covers`) with no warning.
|
|
56
|
+
A platform may override to gate coverage on the wheel's *source* — e.g.
|
|
57
|
+
Linux accepts a plain ``linux_*`` (no-glibc-promise) wheel only when it
|
|
58
|
+
was vendored via ``find_links``, and warns when it does.
|
|
59
|
+
"""
|
|
60
|
+
return self.wheel_covers(wheel.platform_tag, archs), None
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def runtime_provider(self, config: Config) -> RuntimeProvider:
|
|
64
|
+
"""The default runtime provider for this platform."""
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def coverage_error(self, name: str, missing: list[str]) -> str:
|
|
68
|
+
"""Message when a compiled package is missing a required variant."""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def wheel_scope_hint(self) -> str:
|
|
72
|
+
"""The vendored-wheel scope hint (platform-appropriate example path)."""
|
|
73
|
+
|
|
74
|
+
# --- fields with cross-platform defaults (overlay-name agnostic accessors) ---
|
|
75
|
+
|
|
76
|
+
def wheel_resolver(self) -> WheelResolver:
|
|
77
|
+
return get_wheel_resolver("pip")
|
|
78
|
+
|
|
79
|
+
def extra_index_urls(self, config: Config) -> tuple[str, ...]:
|
|
80
|
+
return tuple(getattr(self.overlay(config), "extra_index_urls", ()) or ())
|
|
81
|
+
|
|
82
|
+
def find_links(self, config: Config) -> tuple[str, ...]:
|
|
83
|
+
return tuple(getattr(self.overlay(config), "find_links", ()) or ())
|
|
84
|
+
|
|
85
|
+
def exclude(self, config: Config) -> tuple[str, ...]:
|
|
86
|
+
return tuple(getattr(self.overlay(config), "exclude", ()) or ())
|
|
87
|
+
|
|
88
|
+
def archs(self, config: Config) -> tuple[str, ...]:
|
|
89
|
+
return tuple(v.arch for v in self.variants(config))
|
|
90
|
+
|
|
91
|
+
def schema_version(self, config: Config) -> int:
|
|
92
|
+
return int(getattr(self.overlay(config), "schema_version", 1))
|
|
93
|
+
|
|
94
|
+
def declared_floor(self, config: Config) -> str | None:
|
|
95
|
+
"""The user-declared OS floor to validate against the runtime floor."""
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def floor_error(
|
|
99
|
+
self, declared: str, runtime_floor: str, version: str
|
|
100
|
+
) -> str: # pragma: no cover - overridden where a floor exists
|
|
101
|
+
return (
|
|
102
|
+
f"the declared OS floor {declared} is below the {runtime_floor} floor "
|
|
103
|
+
f"required by the Python {version} runtime."
|
|
104
|
+
)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Generic wheel resolver for the wheel+runtime family.
|
|
2
|
+
|
|
3
|
+
Resolves ``[project].dependencies`` to fully-pinned wheels, host-independent: pip
|
|
4
|
+
runs once per *variant* (an arch/ABI + its pip ``--platform`` tag) and the
|
|
5
|
+
results are merged. A wheel that satisfies several variants (e.g. macOS
|
|
6
|
+
``universal2``) is deduplicated by filename. The pip mechanics are identical to
|
|
7
|
+
the iOS resolver (dry-run ``--report`` per platform tag); only the tag list — and
|
|
8
|
+
the coverage rule, which lives in the platform profile — differ. The
|
|
9
|
+
platform-neutral pip helpers are shared from ``kivyforge.lock.resolver``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Protocol
|
|
21
|
+
|
|
22
|
+
from ..model import canonical_name
|
|
23
|
+
from ..resolver import (
|
|
24
|
+
MIN_PIP_VERSION,
|
|
25
|
+
_dep_names,
|
|
26
|
+
abi_tags,
|
|
27
|
+
pip_python_version,
|
|
28
|
+
pip_version,
|
|
29
|
+
version_str,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WheelResolverError(Exception):
|
|
34
|
+
"""A resolution failure (missing variant, non-wheel, backend error)."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Variant:
|
|
39
|
+
"""One build variant: an arch/ABI identifier + its pip ``--platform`` tag(s).
|
|
40
|
+
|
|
41
|
+
``platform_tag`` is the primary/identity tag. ``extra_platform_tags`` lets a
|
|
42
|
+
variant request additional pip ``--platform`` tags in the *same* pip
|
|
43
|
+
invocation — Linux needs this because pip does not expand the manylinux
|
|
44
|
+
hierarchy from a single explicit ``--platform`` (see linux-spec), so the
|
|
45
|
+
profile passes the whole compatible tag ladder and pip resolves one
|
|
46
|
+
consistent version accepting a wheel matching any of them. macOS variants
|
|
47
|
+
carry a single tag (``extra_platform_tags`` empty).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
arch: str
|
|
51
|
+
platform_tag: str
|
|
52
|
+
extra_platform_tags: tuple[str, ...] = ()
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def request_tags(self) -> tuple[str, ...]:
|
|
56
|
+
return (self.platform_tag, *self.extra_platform_tags)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class ResolvedWheel:
|
|
61
|
+
filename: str
|
|
62
|
+
url: str
|
|
63
|
+
sha256: str
|
|
64
|
+
upload_time: str | None = None
|
|
65
|
+
size: int | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ResolvedPackage:
|
|
70
|
+
name: str
|
|
71
|
+
version: str
|
|
72
|
+
wheels: list[ResolvedWheel] = field(default_factory=list)
|
|
73
|
+
requires_python: str | None = None
|
|
74
|
+
dependencies: list[str] = field(default_factory=list)
|
|
75
|
+
source_index: str | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class WheelResolver(Protocol):
|
|
79
|
+
def resolve(
|
|
80
|
+
self,
|
|
81
|
+
requirements: list[str],
|
|
82
|
+
*,
|
|
83
|
+
python_version: str,
|
|
84
|
+
variants: tuple[Variant, ...],
|
|
85
|
+
extra_index_urls: list[str],
|
|
86
|
+
find_links: list[str] | None = None,
|
|
87
|
+
offline: bool = False,
|
|
88
|
+
) -> list[ResolvedPackage]:
|
|
89
|
+
"""Resolve requirements to wheels covering every requested variant."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PipWheelResolver:
|
|
94
|
+
"""Default backend: stock pip cross-resolution via ``pip install --report``."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, python_executable: str | None = None) -> None:
|
|
97
|
+
self._python = python_executable or sys.executable
|
|
98
|
+
|
|
99
|
+
def resolve(
|
|
100
|
+
self,
|
|
101
|
+
requirements: list[str],
|
|
102
|
+
*,
|
|
103
|
+
python_version: str,
|
|
104
|
+
variants: tuple[Variant, ...],
|
|
105
|
+
extra_index_urls: list[str],
|
|
106
|
+
find_links: list[str] | None = None,
|
|
107
|
+
offline: bool = False,
|
|
108
|
+
) -> list[ResolvedPackage]:
|
|
109
|
+
if not requirements:
|
|
110
|
+
return []
|
|
111
|
+
self._require_modern_pip()
|
|
112
|
+
abis = abi_tags(python_version)
|
|
113
|
+
merged: dict[str, ResolvedPackage] = {}
|
|
114
|
+
seen_filenames: dict[str, set[str]] = {}
|
|
115
|
+
|
|
116
|
+
for variant in variants:
|
|
117
|
+
report = self._run_report(
|
|
118
|
+
requirements,
|
|
119
|
+
python_version=python_version,
|
|
120
|
+
platform_tags=variant.request_tags,
|
|
121
|
+
abis=abis,
|
|
122
|
+
extra_index_urls=extra_index_urls,
|
|
123
|
+
find_links=find_links or [],
|
|
124
|
+
offline=offline,
|
|
125
|
+
)
|
|
126
|
+
for item in report.get("install", []):
|
|
127
|
+
self._absorb(item, merged, seen_filenames)
|
|
128
|
+
|
|
129
|
+
return list(merged.values())
|
|
130
|
+
|
|
131
|
+
def _require_modern_pip(self) -> None:
|
|
132
|
+
version = pip_version(self._python)
|
|
133
|
+
if version is not None and version < MIN_PIP_VERSION:
|
|
134
|
+
raise WheelResolverError(
|
|
135
|
+
f"kivyforge needs pip >= {version_str(MIN_PIP_VERSION)} to resolve "
|
|
136
|
+
f"wheels, but {self._python} has pip {version_str(version)}.\n"
|
|
137
|
+
f" Upgrade it: {self._python} -m pip install --upgrade pip"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _absorb(self, item, merged, seen_filenames) -> None:
|
|
141
|
+
meta = item.get("metadata", {})
|
|
142
|
+
name = meta.get("name")
|
|
143
|
+
version = meta.get("version")
|
|
144
|
+
download = item.get("download_info", {})
|
|
145
|
+
url = download.get("url", "")
|
|
146
|
+
if not url.endswith(".whl"):
|
|
147
|
+
raise WheelResolverError(
|
|
148
|
+
f"{name} {version} resolved to a non-wheel source ({url}); "
|
|
149
|
+
"kivyforge installs only wheels (no on-host compile)."
|
|
150
|
+
)
|
|
151
|
+
archive = download.get("archive_info", {})
|
|
152
|
+
hashes = archive.get("hashes", {})
|
|
153
|
+
sha256 = hashes.get("sha256", "")
|
|
154
|
+
filename = url.rsplit("/", 1)[-1]
|
|
155
|
+
key = canonical_name(name)
|
|
156
|
+
pkg = merged.get(key)
|
|
157
|
+
if pkg is None:
|
|
158
|
+
pkg = ResolvedPackage(
|
|
159
|
+
name=name,
|
|
160
|
+
version=version,
|
|
161
|
+
requires_python=meta.get("requires_python"),
|
|
162
|
+
dependencies=_dep_names(meta.get("requires_dist", [])),
|
|
163
|
+
)
|
|
164
|
+
merged[key] = pkg
|
|
165
|
+
seen_filenames[key] = set()
|
|
166
|
+
elif version != pkg.version:
|
|
167
|
+
raise WheelResolverError(
|
|
168
|
+
f"{name} resolved to inconsistent versions across variants: "
|
|
169
|
+
f"{pkg.version!r} and {version!r}. Every variant of a package "
|
|
170
|
+
"must pin the same version."
|
|
171
|
+
)
|
|
172
|
+
if filename not in seen_filenames[key]:
|
|
173
|
+
seen_filenames[key].add(filename)
|
|
174
|
+
pkg.wheels.append(ResolvedWheel(filename=filename, url=url, sha256=sha256))
|
|
175
|
+
|
|
176
|
+
def _run_report(
|
|
177
|
+
self,
|
|
178
|
+
requirements: list[str],
|
|
179
|
+
*,
|
|
180
|
+
python_version: str,
|
|
181
|
+
platform_tags: tuple[str, ...],
|
|
182
|
+
abis: tuple[str, ...],
|
|
183
|
+
extra_index_urls: list[str],
|
|
184
|
+
find_links: list[str],
|
|
185
|
+
offline: bool,
|
|
186
|
+
) -> dict:
|
|
187
|
+
with tempfile.TemporaryDirectory(prefix="kivy-wheel-lock-") as tmp:
|
|
188
|
+
report_path = Path(tmp) / "report.json"
|
|
189
|
+
cmd = [
|
|
190
|
+
self._python,
|
|
191
|
+
"-m",
|
|
192
|
+
"pip",
|
|
193
|
+
"install",
|
|
194
|
+
"--dry-run",
|
|
195
|
+
"--ignore-installed",
|
|
196
|
+
"--only-binary=:all:",
|
|
197
|
+
"--python-version",
|
|
198
|
+
pip_python_version(python_version),
|
|
199
|
+
"--implementation",
|
|
200
|
+
"cp",
|
|
201
|
+
"--target",
|
|
202
|
+
str(Path(tmp) / "target"),
|
|
203
|
+
"--report",
|
|
204
|
+
str(report_path),
|
|
205
|
+
]
|
|
206
|
+
# pip accepts --platform multiple times; a wheel matching ANY of the
|
|
207
|
+
# tags is eligible while a single consistent version set is resolved.
|
|
208
|
+
for tag in platform_tags:
|
|
209
|
+
cmd += ["--platform", tag]
|
|
210
|
+
for abi in abis:
|
|
211
|
+
cmd += ["--abi", abi]
|
|
212
|
+
for index in extra_index_urls:
|
|
213
|
+
cmd += ["--extra-index-url", index]
|
|
214
|
+
for link in find_links:
|
|
215
|
+
cmd += ["--find-links", link]
|
|
216
|
+
if offline:
|
|
217
|
+
cmd += ["--no-index"]
|
|
218
|
+
cmd += list(requirements)
|
|
219
|
+
|
|
220
|
+
proc = subprocess.run(
|
|
221
|
+
cmd,
|
|
222
|
+
capture_output=True,
|
|
223
|
+
text=True,
|
|
224
|
+
encoding="utf-8",
|
|
225
|
+
errors="replace",
|
|
226
|
+
)
|
|
227
|
+
if proc.returncode != 0:
|
|
228
|
+
tags = ", ".join(platform_tags)
|
|
229
|
+
raise WheelResolverError(
|
|
230
|
+
f"pip could not resolve wheels for platform tag(s) {tags}.\n"
|
|
231
|
+
f" This usually means a dependency has no wheel for that "
|
|
232
|
+
f"variant upstream.\n"
|
|
233
|
+
f" pip said:\n{_indent(proc.stderr or proc.stdout)}"
|
|
234
|
+
)
|
|
235
|
+
try:
|
|
236
|
+
return json.loads(report_path.read_text(encoding="utf-8"))
|
|
237
|
+
except (OSError, ValueError) as exc:
|
|
238
|
+
raise WheelResolverError(f"could not read pip report: {exc}") from exc
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_wheel_resolver(backend: str = "pip", **kwargs) -> WheelResolver:
|
|
242
|
+
if backend == "pip":
|
|
243
|
+
return PipWheelResolver(**kwargs)
|
|
244
|
+
raise WheelResolverError(f"unknown resolver backend {backend!r} (expected pip)")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _indent(text: str) -> str:
|
|
248
|
+
return "\n".join(f" {line}" for line in (text or "").splitlines())
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Swappable Python-runtime providers for the wheel+runtime family.
|
|
2
|
+
|
|
3
|
+
A ``RuntimeProvider`` resolves a CPython version + arch set to concrete, pinned
|
|
4
|
+
per-arch archives and normalizes them (downstream) to a canonical relocatable
|
|
5
|
+
layout the bundler consumes. Hiding the *source* behind this seam means switching
|
|
6
|
+
from ``python-build-standalone`` (PBS) to python.org's future relocatable builds
|
|
7
|
+
is a re-lock, not a rewrite.
|
|
8
|
+
|
|
9
|
+
``PbsProvider`` is the generic PBS implementation, parameterized by an
|
|
10
|
+
arch→target-triple map so each platform (macOS darwin triples now, Linux triples
|
|
11
|
+
later) is a one-line specialization. PBS embeds a dated release tag in every
|
|
12
|
+
asset filename, so the concrete URL + SHA-256 are discovered via an injectable
|
|
13
|
+
``MetadataFetcher`` (keeping the selection logic hermetically testable); the real
|
|
14
|
+
GitHub-backed fetcher is validated at the phase stop.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Protocol
|
|
22
|
+
|
|
23
|
+
from .model import PythonRuntime, RuntimeArtifact
|
|
24
|
+
|
|
25
|
+
PBS_PROVIDER_NAME = "python-build-standalone"
|
|
26
|
+
|
|
27
|
+
# Per-provider archive layout: the sub-directory an extracted runtime archive
|
|
28
|
+
# places its canonical relocatable CPython tree under. Staging (macOS/Linux)
|
|
29
|
+
# consults this by the runtime's recorded ``provider`` field instead of assuming
|
|
30
|
+
# a fixed layout, so the bundler depends only on the canonical contract and a
|
|
31
|
+
# new provider is registered here in one place (spec 07).
|
|
32
|
+
_PROVIDER_ARCHIVE_ROOTS = {
|
|
33
|
+
PBS_PROVIDER_NAME: "python",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def normalized_runtime_root(provider: str, extracted: Path) -> Path:
|
|
38
|
+
"""Return the canonical relocatable CPython root within *extracted*.
|
|
39
|
+
|
|
40
|
+
Encapsulates each provider's archive layout so bundlers never hard-code a
|
|
41
|
+
specific provider's internals. Raises :class:`RuntimeProviderError` for an
|
|
42
|
+
unregistered provider or an archive that does not match its expected layout.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
root_name = _PROVIDER_ARCHIVE_ROOTS[provider]
|
|
46
|
+
except KeyError:
|
|
47
|
+
raise RuntimeProviderError(
|
|
48
|
+
f"no runtime staging layout registered for provider {provider!r}; "
|
|
49
|
+
f"known providers: {', '.join(sorted(_PROVIDER_ARCHIVE_ROOTS))}."
|
|
50
|
+
) from None
|
|
51
|
+
root = extracted / root_name
|
|
52
|
+
if not root.is_dir():
|
|
53
|
+
raise RuntimeProviderError(
|
|
54
|
+
f"the extracted runtime is missing the expected top-level "
|
|
55
|
+
f"{root_name}/ directory required by the {provider!r} runtime provider."
|
|
56
|
+
)
|
|
57
|
+
return root
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RuntimeProviderError(Exception):
|
|
61
|
+
"""A runtime could not be resolved (unknown arch, missing asset, offline)."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class ReleaseAsset:
|
|
66
|
+
"""One resolved downloadable archive for a single arch."""
|
|
67
|
+
|
|
68
|
+
url: str
|
|
69
|
+
sha256: str
|
|
70
|
+
archive_format: str = "tar.gz"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MetadataFetcher(Protocol):
|
|
74
|
+
"""Resolves a (version, target triple) to a concrete asset (URL + SHA-256)."""
|
|
75
|
+
|
|
76
|
+
def fetch(
|
|
77
|
+
self, version: str, target_triple: str, *, offline: bool = False
|
|
78
|
+
) -> ReleaseAsset: ...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RuntimeProvider(Protocol):
|
|
82
|
+
name: str
|
|
83
|
+
|
|
84
|
+
def resolve(
|
|
85
|
+
self, version: str, archs: tuple[str, ...], *, offline: bool = False
|
|
86
|
+
) -> PythonRuntime: ...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PbsProvider:
|
|
90
|
+
"""python-build-standalone provider, parameterized by an arch→triple map."""
|
|
91
|
+
|
|
92
|
+
name = PBS_PROVIDER_NAME
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
triples: dict[str, str],
|
|
97
|
+
metadata_fetcher: MetadataFetcher | None = None,
|
|
98
|
+
*,
|
|
99
|
+
floor: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
self._triples = triples
|
|
102
|
+
# The default GitHub-backed fetcher is created lazily so tests that
|
|
103
|
+
# inject a fake never import the network layer.
|
|
104
|
+
self._fetcher = metadata_fetcher
|
|
105
|
+
self._floor = floor
|
|
106
|
+
|
|
107
|
+
def resolve(
|
|
108
|
+
self, version: str, archs: tuple[str, ...], *, offline: bool = False
|
|
109
|
+
) -> PythonRuntime:
|
|
110
|
+
if not archs:
|
|
111
|
+
raise RuntimeProviderError("no architectures requested for the runtime")
|
|
112
|
+
unknown = [a for a in archs if a not in self._triples]
|
|
113
|
+
if unknown:
|
|
114
|
+
valid = ", ".join(sorted(self._triples))
|
|
115
|
+
raise RuntimeProviderError(
|
|
116
|
+
f"python-build-standalone has no build for arch(es) {unknown}; "
|
|
117
|
+
f"valid values are: {valid}."
|
|
118
|
+
)
|
|
119
|
+
fetcher = self._fetcher or _default_fetcher()
|
|
120
|
+
artifacts: list[RuntimeArtifact] = []
|
|
121
|
+
for arch in archs:
|
|
122
|
+
triple = self._triples[arch]
|
|
123
|
+
try:
|
|
124
|
+
asset = fetcher.fetch(version, triple, offline=offline)
|
|
125
|
+
except RuntimeProviderError:
|
|
126
|
+
raise
|
|
127
|
+
except Exception as exc: # noqa: BLE001 — normalize fetcher failures
|
|
128
|
+
raise RuntimeProviderError(
|
|
129
|
+
f"could not resolve a python-build-standalone {version} build "
|
|
130
|
+
f"for {arch} ({triple}): {exc}"
|
|
131
|
+
) from exc
|
|
132
|
+
artifacts.append(
|
|
133
|
+
RuntimeArtifact(
|
|
134
|
+
arch=arch,
|
|
135
|
+
url=asset.url,
|
|
136
|
+
sha256=asset.sha256,
|
|
137
|
+
archive_format=asset.archive_format,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
return PythonRuntime(
|
|
141
|
+
provider=self.name,
|
|
142
|
+
version=version,
|
|
143
|
+
artifacts=tuple(artifacts),
|
|
144
|
+
floor=self._floor,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def pbs_asset_glob(version: str, target_triple: str) -> str:
|
|
149
|
+
"""The PBS ``install_only`` archive filename pattern for a version + triple.
|
|
150
|
+
|
|
151
|
+
PBS embeds a dated release tag (e.g.
|
|
152
|
+
``cpython-3.15.0+20250115-aarch64-apple-darwin-install_only.tar.gz``); this
|
|
153
|
+
returns the tag-globbed stem the metadata lookup matches.
|
|
154
|
+
"""
|
|
155
|
+
return f"cpython-{version}+*-{target_triple}-install_only.tar.gz"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _default_fetcher() -> MetadataFetcher:
|
|
159
|
+
from .pbs_github import GithubPbsMetadataFetcher
|
|
160
|
+
|
|
161
|
+
return GithubPbsMetadataFetcher()
|