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,252 @@
|
|
|
1
|
+
"""Gradle/Maven channel resolution for the lock (android/02, channel 4).
|
|
2
|
+
|
|
3
|
+
``kivyforge lock`` emits the declared coordinates into a **scratch** Gradle
|
|
4
|
+
project, runs Gradle with dependency verification metadata generation
|
|
5
|
+
(``--write-verification-metadata sha256``) against a task that force-resolves
|
|
6
|
+
the runtime classpath, and parses the resulting
|
|
7
|
+
``gradle/verification-metadata.xml`` into ``[[tool.kivyforge.gradle.resolved]]``
|
|
8
|
+
pins. The values live **in the lock**; ``kivyforge build`` later materializes
|
|
9
|
+
``app/gradle.lockfile`` + ``gradle/verification-metadata.xml`` into the
|
|
10
|
+
generated project from these pins (android/02 §gradle pins).
|
|
11
|
+
|
|
12
|
+
The Gradle invocation is behind the ``GradleMavenResolver`` protocol so unit
|
|
13
|
+
tests inject a fake and stay hermetic; the metadata parser is pure and tested
|
|
14
|
+
against recorded XML. A JDK is required only when Maven dependencies are
|
|
15
|
+
actually declared (android/06 §lock).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import shutil
|
|
21
|
+
import subprocess
|
|
22
|
+
import tempfile
|
|
23
|
+
import xml.etree.ElementTree as ET
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Protocol
|
|
26
|
+
|
|
27
|
+
from kivyforge.config.model import AndroidGradleConfig
|
|
28
|
+
|
|
29
|
+
from .model import GradleArtifact, GradlePins, GradleResolvedModule
|
|
30
|
+
|
|
31
|
+
_VERIFICATION_NS = "{https://schema.gradle.org/dependency-verification}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MavenResolverError(Exception):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GradleMavenResolver(Protocol):
|
|
39
|
+
def resolve(
|
|
40
|
+
self, gradle: AndroidGradleConfig, *, offline: bool = False
|
|
41
|
+
) -> tuple[GradleResolvedModule, ...]: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_gradle_pins(
|
|
45
|
+
gradle: AndroidGradleConfig,
|
|
46
|
+
*,
|
|
47
|
+
resolver: GradleMavenResolver | None = None,
|
|
48
|
+
offline: bool = False,
|
|
49
|
+
) -> GradlePins:
|
|
50
|
+
"""Resolve the declared coordinates into hash-pinned modules.
|
|
51
|
+
|
|
52
|
+
Skipped entirely (empty pins) when no dependencies are declared — the
|
|
53
|
+
common Kivy case never needs a JDK at lock time.
|
|
54
|
+
"""
|
|
55
|
+
if not gradle.dependencies:
|
|
56
|
+
return GradlePins()
|
|
57
|
+
backend = resolver or ScratchProjectResolver()
|
|
58
|
+
resolved = backend.resolve(gradle, offline=offline)
|
|
59
|
+
return GradlePins(
|
|
60
|
+
dependencies=tuple(gradle.dependencies),
|
|
61
|
+
repositories=tuple(gradle.repositories),
|
|
62
|
+
resolved=resolved,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_verification_metadata(xml_text: str) -> tuple[GradleResolvedModule, ...]:
|
|
67
|
+
"""Parse Gradle's ``verification-metadata.xml`` into resolved-module pins.
|
|
68
|
+
|
|
69
|
+
The document shape (Gradle dependency-verification schema):
|
|
70
|
+
``<verification-metadata><components><component group= name= version=>
|
|
71
|
+
<artifact name=...><sha256 value=.../></artifact>...`` — one component per
|
|
72
|
+
resolved module, one artifact per downloaded file.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
root = ET.fromstring(xml_text)
|
|
76
|
+
except ET.ParseError as exc:
|
|
77
|
+
raise MavenResolverError(
|
|
78
|
+
f"could not parse Gradle verification-metadata.xml: {exc}"
|
|
79
|
+
) from exc
|
|
80
|
+
modules: list[GradleResolvedModule] = []
|
|
81
|
+
for component in root.iter(f"{_VERIFICATION_NS}component"):
|
|
82
|
+
group = component.get("group", "")
|
|
83
|
+
name = component.get("name", "")
|
|
84
|
+
version = component.get("version", "")
|
|
85
|
+
artifacts: list[GradleArtifact] = []
|
|
86
|
+
for artifact in component.findall(f"{_VERIFICATION_NS}artifact"):
|
|
87
|
+
sha = artifact.find(f"{_VERIFICATION_NS}sha256")
|
|
88
|
+
value = sha.get("value", "") if sha is not None else ""
|
|
89
|
+
if value:
|
|
90
|
+
artifacts.append(
|
|
91
|
+
GradleArtifact(name=artifact.get("name", ""), sha256=value)
|
|
92
|
+
)
|
|
93
|
+
modules.append(
|
|
94
|
+
GradleResolvedModule(
|
|
95
|
+
coordinate=f"{group}:{name}:{version}",
|
|
96
|
+
artifacts=tuple(sorted(artifacts, key=lambda a: a.name)),
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
modules.sort(key=lambda m: m.coordinate)
|
|
100
|
+
return tuple(modules)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --------------------------------------------------------------------------- #
|
|
104
|
+
# Default backend: a scratch Gradle project.
|
|
105
|
+
# --------------------------------------------------------------------------- #
|
|
106
|
+
|
|
107
|
+
_SETTINGS_GRADLE = """\
|
|
108
|
+
dependencyResolutionManagement {
|
|
109
|
+
repositories {
|
|
110
|
+
google()
|
|
111
|
+
mavenCentral()
|
|
112
|
+
%(extra_repos)s
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
rootProject.name = "kivyforge-maven-lock"
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
# A plain JVM project with an Android-attribute-tolerant configuration: the
|
|
119
|
+
# lenient artifact view lets .aar-packaged AndroidX modules resolve without
|
|
120
|
+
# AGP, since the lock only needs the *graph + bytes*, not a compiled classpath.
|
|
121
|
+
_BUILD_GRADLE = """\
|
|
122
|
+
plugins { id 'java-library' }
|
|
123
|
+
|
|
124
|
+
configurations {
|
|
125
|
+
kivyforgeLock {
|
|
126
|
+
canBeConsumed = false
|
|
127
|
+
canBeResolved = true
|
|
128
|
+
attributes {
|
|
129
|
+
attribute(Attribute.of('artifactType', String), 'jar')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
dependencies {
|
|
135
|
+
%(dependencies)s
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Force full artifact download so verification metadata covers every file.
|
|
139
|
+
tasks.register('kivyforgeResolveAll') {
|
|
140
|
+
doLast {
|
|
141
|
+
def seen = configurations.kivyforgeLock.incoming.artifactView {
|
|
142
|
+
lenient = true
|
|
143
|
+
}.files.files
|
|
144
|
+
println "kivyforge: resolved ${seen.size()} artifact files"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ScratchProjectResolver:
|
|
151
|
+
"""Run Gradle in a temp project and harvest verification metadata.
|
|
152
|
+
|
|
153
|
+
Uses a standalone ``gradle`` if one is on PATH; otherwise stages kivyforge's
|
|
154
|
+
vendored Gradle wrapper into the scratch project and runs it (the same
|
|
155
|
+
wrapper `build` uses), so a project only needs a JDK — not a separate Gradle
|
|
156
|
+
install — for the Maven lock.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
_WRAPPER_DIR = Path(__file__).parent.parent / "gradle_wrapper"
|
|
160
|
+
|
|
161
|
+
def __init__(self, gradle_executable: str | None = None) -> None:
|
|
162
|
+
self._gradle = gradle_executable
|
|
163
|
+
|
|
164
|
+
def resolve(
|
|
165
|
+
self, gradle: AndroidGradleConfig, *, offline: bool = False
|
|
166
|
+
) -> tuple[GradleResolvedModule, ...]:
|
|
167
|
+
with tempfile.TemporaryDirectory(prefix="kivyforge-maven-") as tmp:
|
|
168
|
+
root = Path(tmp)
|
|
169
|
+
extra = "\n".join(
|
|
170
|
+
f" maven {{ url = uri({_groovy_str(u)}) }}"
|
|
171
|
+
for u in gradle.repositories
|
|
172
|
+
)
|
|
173
|
+
deps = "\n".join(
|
|
174
|
+
f" kivyforgeLock {_groovy_str(c)}" for c in gradle.dependencies
|
|
175
|
+
)
|
|
176
|
+
(root / "settings.gradle").write_text(
|
|
177
|
+
_SETTINGS_GRADLE % {"extra_repos": extra}, encoding="utf-8"
|
|
178
|
+
)
|
|
179
|
+
(root / "build.gradle").write_text(
|
|
180
|
+
_BUILD_GRADLE % {"dependencies": deps}, encoding="utf-8"
|
|
181
|
+
)
|
|
182
|
+
exe = self._gradle_command(root)
|
|
183
|
+
cmd = [
|
|
184
|
+
*exe,
|
|
185
|
+
"--no-daemon",
|
|
186
|
+
"--console=plain",
|
|
187
|
+
"--write-verification-metadata",
|
|
188
|
+
"sha256",
|
|
189
|
+
"kivyforgeResolveAll",
|
|
190
|
+
]
|
|
191
|
+
if offline:
|
|
192
|
+
cmd.append("--offline")
|
|
193
|
+
proc = subprocess.run(cmd, cwd=root, capture_output=True, text=True)
|
|
194
|
+
if proc.returncode != 0:
|
|
195
|
+
raise MavenResolverError(
|
|
196
|
+
"Gradle could not resolve the declared Maven coordinates.\n"
|
|
197
|
+
f" Gradle said:\n{_indent(proc.stderr or proc.stdout)}"
|
|
198
|
+
)
|
|
199
|
+
metadata = root / "gradle" / "verification-metadata.xml"
|
|
200
|
+
if not metadata.is_file():
|
|
201
|
+
raise MavenResolverError(
|
|
202
|
+
"Gradle wrote no verification-metadata.xml; cannot pin the "
|
|
203
|
+
"resolved Maven graph."
|
|
204
|
+
)
|
|
205
|
+
return parse_verification_metadata(metadata.read_text(encoding="utf-8"))
|
|
206
|
+
|
|
207
|
+
def _gradle_command(self, root: Path) -> list[str]:
|
|
208
|
+
"""A standalone gradle if present, else the vendored wrapper in ``root``."""
|
|
209
|
+
if self._gradle:
|
|
210
|
+
return [self._gradle]
|
|
211
|
+
standalone = shutil.which("gradle")
|
|
212
|
+
if standalone:
|
|
213
|
+
return [standalone]
|
|
214
|
+
jar = self._WRAPPER_DIR / "gradle-wrapper.jar"
|
|
215
|
+
if not jar.is_file():
|
|
216
|
+
raise MavenResolverError(
|
|
217
|
+
"[tool.kivy.android.gradle].dependencies is declared, but "
|
|
218
|
+
"neither a standalone `gradle` nor the vendored wrapper is "
|
|
219
|
+
"available (a JDK is still required for this channel; "
|
|
220
|
+
"android/06 §lock)."
|
|
221
|
+
)
|
|
222
|
+
import os as _os
|
|
223
|
+
|
|
224
|
+
wrapper = root / "gradle" / "wrapper"
|
|
225
|
+
wrapper.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
shutil.copy2(jar, wrapper / "gradle-wrapper.jar")
|
|
227
|
+
(wrapper / "gradle-wrapper.properties").write_text(
|
|
228
|
+
"distributionBase=GRADLE_USER_HOME\n"
|
|
229
|
+
"distributionPath=wrapper/dists\n"
|
|
230
|
+
"distributionUrl=https\\://services.gradle.org/distributions/"
|
|
231
|
+
"gradle-8.11.1-bin.zip\n"
|
|
232
|
+
"zipStoreBase=GRADLE_USER_HOME\n"
|
|
233
|
+
"zipStorePath=wrapper/dists\n",
|
|
234
|
+
encoding="utf-8",
|
|
235
|
+
)
|
|
236
|
+
for script in ("gradlew", "gradlew.bat"):
|
|
237
|
+
src = self._WRAPPER_DIR / script
|
|
238
|
+
if src.is_file():
|
|
239
|
+
dst = root / script
|
|
240
|
+
shutil.copy2(src, dst)
|
|
241
|
+
if script == "gradlew":
|
|
242
|
+
dst.chmod(dst.stat().st_mode | 0o111)
|
|
243
|
+
return [str(root / ("gradlew.bat" if _os.name == "nt" else "gradlew"))]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _groovy_str(value: str) -> str:
|
|
247
|
+
escaped = value.replace("\\", "\\\\").replace("'", "\\'")
|
|
248
|
+
return f"'{escaped}'"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _indent(text: str) -> str:
|
|
252
|
+
return "\n".join(f" {line}" for line in (text or "").splitlines())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""In-memory model of ``pylock.android.toml`` (android/02).
|
|
2
|
+
|
|
3
|
+
PEP 751-shaped ``[[packages]]`` (shared ``LockedPackage``/``LockedWheel``) plus
|
|
4
|
+
the Android ``[tool.kivyforge]`` extension: the per-ABI python.org runtime,
|
|
5
|
+
``.aar``/``.jar`` pins, the Gradle/Maven resolved graph, and the
|
|
6
|
+
``include_files`` drift pins.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from kivyforge.lock.model import LOCK_VERSION, LockedPackage
|
|
14
|
+
|
|
15
|
+
CREATED_BY = "kivyforge"
|
|
16
|
+
TOOL_SCHEMA_VERSION = 1
|
|
17
|
+
# Android's CPython floor (Tier 3; android/02 requires-python fallback).
|
|
18
|
+
DEFAULT_REQUIRES_PYTHON = ">=3.14"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class PythonAndroidRuntime:
|
|
23
|
+
"""One ``[[tool.kivyforge.python_android]]`` entry (per ABI)."""
|
|
24
|
+
|
|
25
|
+
version: str
|
|
26
|
+
abi: str
|
|
27
|
+
sha256: str
|
|
28
|
+
min_api: int
|
|
29
|
+
url: str | None = None
|
|
30
|
+
path: str | None = None
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
if bool(self.url) == bool(self.path):
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"python_android {self.abi!r} must have exactly one of url/path"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class LockedAndroidLib:
|
|
41
|
+
"""One ``[[tool.kivyforge.android_libs]]`` entry (.aar/.jar, channel 3)."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
kind: str # "aar" | "jar"
|
|
45
|
+
version: str
|
|
46
|
+
sha256: str
|
|
47
|
+
url: str | None = None
|
|
48
|
+
path: str | None = None
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
if bool(self.url) == bool(self.path):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"android lib {self.name!r} must have exactly one of url/path"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class GradleArtifact:
|
|
59
|
+
"""One downloaded artifact of a resolved Maven module."""
|
|
60
|
+
|
|
61
|
+
name: str
|
|
62
|
+
sha256: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class GradleResolvedModule:
|
|
67
|
+
"""One ``[[tool.kivyforge.gradle.resolved]]`` entry."""
|
|
68
|
+
|
|
69
|
+
coordinate: str
|
|
70
|
+
artifacts: tuple[GradleArtifact, ...]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class GradlePins:
|
|
75
|
+
"""``[tool.kivyforge.gradle]`` — declared coordinates + resolved graph."""
|
|
76
|
+
|
|
77
|
+
dependencies: tuple[str, ...] = ()
|
|
78
|
+
repositories: tuple[str, ...] = ()
|
|
79
|
+
resolved: tuple[GradleResolvedModule, ...] = ()
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def declared(self) -> bool:
|
|
83
|
+
return bool(self.dependencies)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class LockedIncludeFile:
|
|
88
|
+
"""One ``[[tool.kivyforge.include_files]]`` drift pin."""
|
|
89
|
+
|
|
90
|
+
source: str
|
|
91
|
+
dest: str
|
|
92
|
+
sha256: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class AndroidLockfile:
|
|
97
|
+
requires_python: str
|
|
98
|
+
packages: tuple[LockedPackage, ...]
|
|
99
|
+
python_android: tuple[PythonAndroidRuntime, ...]
|
|
100
|
+
kivyforge_version: str
|
|
101
|
+
generated_at: str
|
|
102
|
+
pyproject_sha256: str
|
|
103
|
+
tool_kivy_android_schema_version: int
|
|
104
|
+
kivy_generation: int
|
|
105
|
+
android_libs: tuple[LockedAndroidLib, ...] = ()
|
|
106
|
+
gradle: GradlePins = GradlePins()
|
|
107
|
+
include_files: tuple[LockedIncludeFile, ...] = ()
|
|
108
|
+
schema_version: int = TOOL_SCHEMA_VERSION
|
|
109
|
+
lock_version: str = LOCK_VERSION
|
|
110
|
+
created_by: str = CREATED_BY
|
|
111
|
+
extras: tuple[str, ...] = ()
|
|
112
|
+
dependency_groups: tuple[str, ...] = ()
|
|
113
|
+
default_groups: tuple[str, ...] = ()
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Resolve python.org Android runtime metadata for the lock (android/02).
|
|
2
|
+
|
|
3
|
+
``kivyforge lock`` pins, per targeted ABI, the Android embeddable package's
|
|
4
|
+
URL + SHA-256 and its minimum-API floor into ``[[tool.kivyforge.python_android]]``.
|
|
5
|
+
Modeled on the iOS ``PythonOrgProvider`` — same cache/offline/retry posture —
|
|
6
|
+
but per-ABI (the runtime ships one artifact per ABI, not a fat xcframework).
|
|
7
|
+
|
|
8
|
+
Artifact naming is **discovered from the release's file listing**, not
|
|
9
|
+
hardcoded: the observed 3.14.6 names are
|
|
10
|
+
``python-3.14.6-{aarch64,x86_64}-linux-android.tar.gz`` (see
|
|
11
|
+
docs/design/dev/android-loadmodel-findings.md §runtime-package facts, which
|
|
12
|
+
corrected the illustrative URL in android/02). The known pattern serves as a
|
|
13
|
+
fallback when the listing is unreachable but the artifact itself is.
|
|
14
|
+
|
|
15
|
+
The min-API floor is read from ``android-env.sh`` inside the tarball
|
|
16
|
+
(``ANDROID_API_LEVEL:=24``) — the same source python.org's own testbed Gradle
|
|
17
|
+
project reads it from, so there is no per-version table to maintain.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
import re
|
|
24
|
+
import tarfile
|
|
25
|
+
import tempfile
|
|
26
|
+
import time
|
|
27
|
+
import urllib.request
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Protocol
|
|
31
|
+
|
|
32
|
+
from kivyforge.artifacts.cache import ArtifactCache
|
|
33
|
+
from kivyforge.artifacts.verify import sha256_file
|
|
34
|
+
|
|
35
|
+
RELEASE_LISTING_URL = "https://www.python.org/ftp/python/{version}/"
|
|
36
|
+
|
|
37
|
+
# ABI (wheel-tag spelling) -> the arch component in python.org's filenames.
|
|
38
|
+
ABI_TO_PYTHON_ORG_ARCH = {"arm64_v8a": "aarch64", "x86_64": "x86_64"}
|
|
39
|
+
|
|
40
|
+
_FALLBACK_NAME = "python-{version}-{arch}-linux-android.tar.gz"
|
|
41
|
+
_ANDROID_TARBALL_RE = re.compile(
|
|
42
|
+
r'href="(python-(?P<version>[^"]+?)-(?P<arch>aarch64|x86_64)'
|
|
43
|
+
r'-linux-android\.tar\.gz)"'
|
|
44
|
+
)
|
|
45
|
+
_API_LEVEL_RE = re.compile(r"ANDROID_API_LEVEL:?=\s*(\d+)")
|
|
46
|
+
|
|
47
|
+
_DOWNLOAD_ATTEMPTS = 4
|
|
48
|
+
_RETRY_BACKOFF_SEC = (1.0, 2.0, 4.0)
|
|
49
|
+
# Returned when android-env.sh is absent or unparseable; matches the runtime's
|
|
50
|
+
# documented floor (android/01 min_sdk).
|
|
51
|
+
_MIN_API_FALLBACK = 24
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PythonAndroidError(Exception):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class PythonAndroidInfo:
|
|
60
|
+
"""One per-ABI runtime pin for ``[[tool.kivyforge.python_android]]``."""
|
|
61
|
+
|
|
62
|
+
version: str
|
|
63
|
+
abi: str
|
|
64
|
+
url: str
|
|
65
|
+
sha256: str
|
|
66
|
+
min_api: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PythonAndroidProvider(Protocol):
|
|
70
|
+
def get(
|
|
71
|
+
self, version: str, abi: str, *, offline: bool = False
|
|
72
|
+
) -> PythonAndroidInfo: ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def read_min_api(archive_path: str | Path) -> int:
|
|
76
|
+
"""Read ``ANDROID_API_LEVEL`` from ``android-env.sh`` inside the tarball."""
|
|
77
|
+
try:
|
|
78
|
+
with open(archive_path, "rb") as f:
|
|
79
|
+
return _read_min_api_from_fileobj(f)
|
|
80
|
+
except OSError:
|
|
81
|
+
return _MIN_API_FALLBACK
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _read_min_api_from_fileobj(fileobj) -> int:
|
|
85
|
+
"""Same as :func:`read_min_api`, but reads an already-open, seekable file.
|
|
86
|
+
|
|
87
|
+
``_download`` reads it via its own live ``NamedTemporaryFile`` handle
|
|
88
|
+
rather than reopening the path — reopening a still-open ``NamedTemporaryFile``
|
|
89
|
+
by name is a documented POSIX-only guarantee (it silently fails on Windows).
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
with tarfile.open(fileobj=fileobj, mode="r:gz") as tf:
|
|
93
|
+
for member in tf.getmembers():
|
|
94
|
+
if not member.name.endswith("android-env.sh"):
|
|
95
|
+
continue
|
|
96
|
+
f = tf.extractfile(member)
|
|
97
|
+
if f is None:
|
|
98
|
+
continue
|
|
99
|
+
match = _API_LEVEL_RE.search(f.read().decode("utf-8", "replace"))
|
|
100
|
+
if match:
|
|
101
|
+
return int(match.group(1))
|
|
102
|
+
except Exception: # noqa: BLE001 — malformed archive, test fixture, etc.
|
|
103
|
+
pass
|
|
104
|
+
return _MIN_API_FALLBACK
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PythonOrgAndroidProvider:
|
|
108
|
+
"""Default provider: discover the artifact from the release file listing."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, *, cache: ArtifactCache | None = None) -> None:
|
|
111
|
+
self._cache = cache or ArtifactCache()
|
|
112
|
+
# version -> {arch -> filename}, from one listing fetch per version.
|
|
113
|
+
self._listing: dict[str, dict[str, str]] = {}
|
|
114
|
+
|
|
115
|
+
def get(
|
|
116
|
+
self, version: str, abi: str, *, offline: bool = False
|
|
117
|
+
) -> PythonAndroidInfo:
|
|
118
|
+
arch = ABI_TO_PYTHON_ORG_ARCH.get(abi)
|
|
119
|
+
if arch is None:
|
|
120
|
+
raise PythonAndroidError(
|
|
121
|
+
f"unknown Android ABI {abi!r}; expected one of "
|
|
122
|
+
f"{sorted(ABI_TO_PYTHON_ORG_ARCH)}"
|
|
123
|
+
)
|
|
124
|
+
filename = self._filename(version, arch, offline=offline)
|
|
125
|
+
url = RELEASE_LISTING_URL.format(version=version) + filename
|
|
126
|
+
|
|
127
|
+
cached = self._cache.find_by_filename(filename)
|
|
128
|
+
if cached is not None:
|
|
129
|
+
return PythonAndroidInfo(
|
|
130
|
+
version=version,
|
|
131
|
+
abi=abi,
|
|
132
|
+
url=url,
|
|
133
|
+
sha256=sha256_file(cached),
|
|
134
|
+
min_api=read_min_api(cached),
|
|
135
|
+
)
|
|
136
|
+
if offline:
|
|
137
|
+
raise PythonAndroidError(
|
|
138
|
+
f"python.org Android runtime {version} ({abi}) is not in the "
|
|
139
|
+
f"artifact cache ({filename}).\n"
|
|
140
|
+
f" Run `kivyforge lock` online once, then retry with --offline."
|
|
141
|
+
)
|
|
142
|
+
sha256, min_api = self._download(url, version, abi)
|
|
143
|
+
return PythonAndroidInfo(
|
|
144
|
+
version=version, abi=abi, url=url, sha256=sha256, min_api=min_api
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# ----------------------------------------------------------------- #
|
|
148
|
+
def _filename(self, version: str, arch: str, *, offline: bool) -> str:
|
|
149
|
+
fallback = _FALLBACK_NAME.format(version=version, arch=arch)
|
|
150
|
+
if offline:
|
|
151
|
+
return fallback
|
|
152
|
+
listing = self._listing.get(version)
|
|
153
|
+
if listing is None:
|
|
154
|
+
listing = self._fetch_listing(version)
|
|
155
|
+
self._listing[version] = listing
|
|
156
|
+
return listing.get(arch, fallback)
|
|
157
|
+
|
|
158
|
+
def _fetch_listing(self, version: str) -> dict[str, str]:
|
|
159
|
+
url = RELEASE_LISTING_URL.format(version=version)
|
|
160
|
+
try:
|
|
161
|
+
with urllib.request.urlopen(url) as resp: # noqa: S310
|
|
162
|
+
html = resp.read().decode("utf-8", "replace")
|
|
163
|
+
except OSError:
|
|
164
|
+
# The artifact fetch itself will surface a clear error if the
|
|
165
|
+
# fallback name is also wrong.
|
|
166
|
+
return {}
|
|
167
|
+
found: dict[str, str] = {}
|
|
168
|
+
for match in _ANDROID_TARBALL_RE.finditer(html):
|
|
169
|
+
found[match.group("arch")] = match.group(1)
|
|
170
|
+
return found
|
|
171
|
+
|
|
172
|
+
def _download(self, url: str, version: str, abi: str) -> tuple[str, int]:
|
|
173
|
+
last_exc: OSError | None = None
|
|
174
|
+
for attempt in range(_DOWNLOAD_ATTEMPTS):
|
|
175
|
+
try:
|
|
176
|
+
digest = hashlib.sha256()
|
|
177
|
+
with tempfile.NamedTemporaryFile(suffix=".tar.gz") as tmp:
|
|
178
|
+
with urllib.request.urlopen(url) as resp: # noqa: S310
|
|
179
|
+
for chunk in iter(lambda: resp.read(1 << 20), b""):
|
|
180
|
+
digest.update(chunk)
|
|
181
|
+
tmp.write(chunk)
|
|
182
|
+
tmp.flush()
|
|
183
|
+
tmp.seek(0)
|
|
184
|
+
min_api = _read_min_api_from_fileobj(tmp)
|
|
185
|
+
return digest.hexdigest(), min_api
|
|
186
|
+
except OSError as exc:
|
|
187
|
+
last_exc = exc
|
|
188
|
+
if attempt < _DOWNLOAD_ATTEMPTS - 1:
|
|
189
|
+
time.sleep(
|
|
190
|
+
_RETRY_BACKOFF_SEC[min(attempt, len(_RETRY_BACKOFF_SEC) - 1)]
|
|
191
|
+
)
|
|
192
|
+
assert last_exc is not None
|
|
193
|
+
raise PythonAndroidError(
|
|
194
|
+
f"could not fetch the python.org Android runtime {version} ({abi}) "
|
|
195
|
+
f"from {url}: {last_exc}\n"
|
|
196
|
+
f" Check the version exists at python.org/downloads/android/ and "
|
|
197
|
+
f"that you are online. Transient errors are retried "
|
|
198
|
+
f"{_DOWNLOAD_ATTEMPTS} times."
|
|
199
|
+
) from last_exc
|