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 @@
|
|
|
1
|
+
"""Stage-step helpers: runtime extract, per-ABI wheel install, jniLibs, bundle."""
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Assemble the ABI-independent Python asset bundle (android/04 §asset bundle).
|
|
2
|
+
|
|
3
|
+
Layout under ``app/src/main/assets/_python_bundle/``:
|
|
4
|
+
|
|
5
|
+
- ``stdlib/`` — the runtime's pure-Python stdlib (no ``lib-dynload``,
|
|
6
|
+
no ``__pycache__``, no ``*.gz`` — AGP auto-decompresses ``.gz`` assets,
|
|
7
|
+
loadmodel findings §corrections #5; ``test``/``idlelib``/``turtledemo``
|
|
8
|
+
excluded for size, matching the proven prototype).
|
|
9
|
+
- ``site-packages/`` — pure-Python content of the installed wheels (``.so``s
|
|
10
|
+
and ``.libs/`` were hoisted into ``jniLibs``).
|
|
11
|
+
- ``app/`` — the user's ``app_dir`` payload.
|
|
12
|
+
- ``bootstrap/`` — the finder module, Kivy's ``_kivy_bootstrap`` contract
|
|
13
|
+
module, and ``ext_manifest.json``.
|
|
14
|
+
- ``VERSION`` — content stamp; a changed bundle re-extracts on-device.
|
|
15
|
+
|
|
16
|
+
``byte_compile``/``strip_source`` (android/01 §build_settings) act on the
|
|
17
|
+
assembled tree just before the stamp is taken, so the stamp always describes
|
|
18
|
+
what actually ships.
|
|
19
|
+
|
|
20
|
+
**ABI-independent by construction, and verified so**: the bundle is assembled
|
|
21
|
+
from the canonical ABI (first in ``abis``); every other ABI's non-``.so``
|
|
22
|
+
site-packages payload must be byte-for-byte identical or the build fails
|
|
23
|
+
naming the path and both hashes (version skew is already rejected at lock
|
|
24
|
+
time; this catches content skew).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import hashlib
|
|
30
|
+
import shutil
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from kivyforge.artifacts.verify import sha256_file
|
|
34
|
+
|
|
35
|
+
# Imported as a module, not by name: ``assemble_bundle`` takes a *parameter*
|
|
36
|
+
# called ``byte_compile`` (the compiler argv), so a bare import would shadow.
|
|
37
|
+
from kivyforge.bundle import pycompile
|
|
38
|
+
|
|
39
|
+
BUNDLE_DIRNAME = "_python_bundle"
|
|
40
|
+
|
|
41
|
+
_STDLIB_EXCLUDED_DIRS = {
|
|
42
|
+
"__pycache__",
|
|
43
|
+
"lib-dynload",
|
|
44
|
+
"site-packages",
|
|
45
|
+
"test",
|
|
46
|
+
"idlelib",
|
|
47
|
+
"turtledemo",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BundleError(Exception):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def assemble_bundle(
|
|
56
|
+
bundle_dir: Path,
|
|
57
|
+
*,
|
|
58
|
+
stdlib_src: Path,
|
|
59
|
+
site_packages_by_abi: dict[str, Path],
|
|
60
|
+
canonical_abi: str,
|
|
61
|
+
app_src: Path,
|
|
62
|
+
finder_source: str,
|
|
63
|
+
kivy_bootstrap_source: str,
|
|
64
|
+
ext_manifest_json: str,
|
|
65
|
+
selftest_source: str = "",
|
|
66
|
+
entry_point: str = "main",
|
|
67
|
+
service_entry_points: dict[str, str] | None = None,
|
|
68
|
+
byte_compile: tuple[str, ...] | None = None,
|
|
69
|
+
strip_source: bool = False,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""Assemble the bundle; returns the content stamp written to ``VERSION``."""
|
|
72
|
+
if bundle_dir.exists():
|
|
73
|
+
shutil.rmtree(bundle_dir)
|
|
74
|
+
bundle_dir.mkdir(parents=True)
|
|
75
|
+
|
|
76
|
+
_copy_stdlib(stdlib_src, bundle_dir / "stdlib")
|
|
77
|
+
canonical_sp = site_packages_by_abi[canonical_abi]
|
|
78
|
+
_copy_pure(canonical_sp, bundle_dir / "site-packages")
|
|
79
|
+
_assert_abi_identity(site_packages_by_abi, canonical_abi)
|
|
80
|
+
_copy_app(
|
|
81
|
+
app_src,
|
|
82
|
+
bundle_dir / "app",
|
|
83
|
+
entry_point=entry_point,
|
|
84
|
+
service_entry_points=service_entry_points or {},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
bootstrap = bundle_dir / "bootstrap"
|
|
88
|
+
bootstrap.mkdir()
|
|
89
|
+
(bootstrap / "_kivyforge_bootstrap.py").write_text(finder_source, encoding="utf-8")
|
|
90
|
+
# Required, not optional: Kivy resolves the Activity by importing this
|
|
91
|
+
# module, so an app built without it would start and then fail the moment
|
|
92
|
+
# Kivy needed the Activity.
|
|
93
|
+
(bootstrap / "_kivy_bootstrap.py").write_text(
|
|
94
|
+
kivy_bootstrap_source, encoding="utf-8"
|
|
95
|
+
)
|
|
96
|
+
if selftest_source:
|
|
97
|
+
(bootstrap / "_kivyforge_selftest.py").write_text(
|
|
98
|
+
selftest_source, encoding="utf-8"
|
|
99
|
+
)
|
|
100
|
+
(bootstrap / "ext_manifest.json").write_text(ext_manifest_json, encoding="utf-8")
|
|
101
|
+
|
|
102
|
+
if byte_compile is not None:
|
|
103
|
+
_byte_compile(bundle_dir, compiler=byte_compile, strip_source=strip_source)
|
|
104
|
+
|
|
105
|
+
stamp = _content_stamp(bundle_dir)
|
|
106
|
+
(bundle_dir / "VERSION").write_text(stamp, encoding="utf-8")
|
|
107
|
+
return stamp
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _byte_compile(
|
|
111
|
+
bundle_dir: Path, *, compiler: tuple[str, ...], strip_source: bool
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Compile the bundle's Python payload to ``.pyc`` in place (android/01).
|
|
114
|
+
|
|
115
|
+
The mechanics live in :mod:`kivyforge.bundle.pycompile` (shared with the
|
|
116
|
+
desktop backends); this only names the payload areas and translates the
|
|
117
|
+
failure into a ``BundleError`` carrying the Android config key.
|
|
118
|
+
|
|
119
|
+
Android compiles **all four** areas, stdlib included — unlike desktop,
|
|
120
|
+
which leaves the stdlib alone. The payload is unpacked to app-private
|
|
121
|
+
storage on first launch, so stripping stdlib source is what keeps the APK
|
|
122
|
+
(and that unpack) small.
|
|
123
|
+
|
|
124
|
+
Paths are stripped to be bundle-relative for the same reason ``_copy_pure``
|
|
125
|
+
drops ``direct_url.json``: a ``.pyc`` records the path it was compiled from,
|
|
126
|
+
so the default would both leak local host paths into the shipped APK and
|
|
127
|
+
make the content stamp differ per machine.
|
|
128
|
+
"""
|
|
129
|
+
trees = [
|
|
130
|
+
bundle_dir / name for name in ("stdlib", "site-packages", "app", "bootstrap")
|
|
131
|
+
]
|
|
132
|
+
try:
|
|
133
|
+
pycompile.byte_compile(
|
|
134
|
+
trees,
|
|
135
|
+
compiler=compiler,
|
|
136
|
+
strip_source=strip_source,
|
|
137
|
+
stripdir=bundle_dir,
|
|
138
|
+
)
|
|
139
|
+
except pycompile.PycompileError as exc:
|
|
140
|
+
raise BundleError(
|
|
141
|
+
f"{exc}\n"
|
|
142
|
+
" Fix it, or set [tool.kivy.android.build_settings].byte_compile "
|
|
143
|
+
"= false."
|
|
144
|
+
) from exc
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _copy_stdlib(src: Path, dest: Path) -> None:
|
|
148
|
+
def ignore(directory: str, names: list[str]) -> list[str]:
|
|
149
|
+
return [n for n in names if n in _STDLIB_EXCLUDED_DIRS or n.endswith(".gz")]
|
|
150
|
+
|
|
151
|
+
shutil.copytree(src, dest, ignore=ignore)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _copy_pure(src: Path, dest: Path) -> None:
|
|
155
|
+
"""Copy an installed site-packages tree minus native payload.
|
|
156
|
+
|
|
157
|
+
``dist-info/direct_url.json`` is dropped entirely: pip writes the install
|
|
158
|
+
source into it (a per-ABI ``file:///`` URL for vendored wheels), which
|
|
159
|
+
both breaks ABI identity by design and would leak local host paths into
|
|
160
|
+
the shipped APK.
|
|
161
|
+
|
|
162
|
+
``bin/`` — pip's console-script launchers — is dropped for the same class
|
|
163
|
+
of reason, and it is worth being explicit because the symptom is
|
|
164
|
+
confusing. Nothing on Android can invoke a console script, and pip
|
|
165
|
+
generates the launchers for the *host*: installing from Windows yields
|
|
166
|
+
Windows ``.exe`` wrappers (``filetype.exe``, ``idna.exe``, …) inside an
|
|
167
|
+
Android APK. They also embed the per-ABI target path, so they differ
|
|
168
|
+
between slices and trip the ABI-identity assertion — which is how this was
|
|
169
|
+
found: ``kivyforge build -p android`` across both ABIs failed with
|
|
170
|
+
"ABI content skew in the pure-Python payload: 'bin/filetype.exe'".
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def ignore(directory: str, names: list[str]) -> list[str]:
|
|
174
|
+
out = [n for n in names if n == "__pycache__" or n.endswith(".so")]
|
|
175
|
+
if Path(directory).name.endswith(".dist-info"):
|
|
176
|
+
out += [n for n in names if n == "direct_url.json"]
|
|
177
|
+
if Path(directory).resolve() == src.resolve():
|
|
178
|
+
# top-level flat .libs/ was hoisted into jniLibs
|
|
179
|
+
out += [n for n in names if n == ".libs"]
|
|
180
|
+
# host-generated console scripts; see the docstring
|
|
181
|
+
out += [n for n in names if n == "bin"]
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
if src.exists():
|
|
185
|
+
shutil.copytree(src, dest, ignore=ignore)
|
|
186
|
+
else:
|
|
187
|
+
dest.mkdir(parents=True)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _require_module(src: Path, entry_point: str, hint: str) -> None:
|
|
191
|
+
"""A dotted entry point must resolve to a module or package in ``app_dir``.
|
|
192
|
+
|
|
193
|
+
``pkg.start`` maps to a nested module; a package entry point is imported
|
|
194
|
+
through its ``__init__.py``. Catching an unresolvable one here beats a black
|
|
195
|
+
screen and an ImportError in logcat.
|
|
196
|
+
"""
|
|
197
|
+
rel = "/".join(entry_point.split("."))
|
|
198
|
+
if not (src / f"{rel}.py").is_file() and not (src / rel / "__init__.py").is_file():
|
|
199
|
+
raise BundleError(f"entry point {rel}.py not found in {src.name}/ ({hint}).")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _copy_app(
|
|
203
|
+
src: Path,
|
|
204
|
+
dest: Path,
|
|
205
|
+
*,
|
|
206
|
+
entry_point: str = "main",
|
|
207
|
+
service_entry_points: dict[str, str],
|
|
208
|
+
) -> None:
|
|
209
|
+
if not src.is_dir():
|
|
210
|
+
raise BundleError(
|
|
211
|
+
f"[tool.kivy].app_dir does not exist or is not a directory: {src}"
|
|
212
|
+
)
|
|
213
|
+
_require_module(src, entry_point, "set [tool.kivy].entry_point")
|
|
214
|
+
# A service whose entry point does not exist starts, comes up, and then dies
|
|
215
|
+
# on the import in a separate process — where nothing is watching.
|
|
216
|
+
for name, service_entry in sorted(service_entry_points.items()):
|
|
217
|
+
_require_module(
|
|
218
|
+
src,
|
|
219
|
+
service_entry,
|
|
220
|
+
f"set entry_point for [[tool.kivy.android.services]] {name!r}",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def ignore(directory: str, names: list[str]) -> list[str]:
|
|
224
|
+
return [n for n in names if n == "__pycache__"]
|
|
225
|
+
|
|
226
|
+
shutil.copytree(src, dest, ignore=ignore)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _assert_abi_identity(
|
|
230
|
+
site_packages_by_abi: dict[str, Path], canonical_abi: str
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Every ABI's non-``.so`` payload must match the canonical ABI's exactly."""
|
|
233
|
+
canonical = _pure_hashes(site_packages_by_abi[canonical_abi])
|
|
234
|
+
for abi, sp in site_packages_by_abi.items():
|
|
235
|
+
if abi == canonical_abi:
|
|
236
|
+
continue
|
|
237
|
+
other = _pure_hashes(sp)
|
|
238
|
+
if other == canonical:
|
|
239
|
+
continue
|
|
240
|
+
for rel in sorted(set(canonical) | set(other)):
|
|
241
|
+
a, b = canonical.get(rel), other.get(rel)
|
|
242
|
+
if a != b:
|
|
243
|
+
raise BundleError(
|
|
244
|
+
f"ABI content skew in the pure-Python payload: {rel!r} is "
|
|
245
|
+
f"{'missing' if a is None else a[:12] + '…'} for "
|
|
246
|
+
f"{canonical_abi} but "
|
|
247
|
+
f"{'missing' if b is None else b[:12] + '…'} for {abi}.\n"
|
|
248
|
+
f" An ABI slice shipping different Python source/data at "
|
|
249
|
+
f"the same version cannot back a single shared bundle "
|
|
250
|
+
f"(android/04 §asset bundle)."
|
|
251
|
+
)
|
|
252
|
+
raise BundleError( # pragma: no cover - loop above always raises
|
|
253
|
+
f"ABI content skew between {canonical_abi} and {abi}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _pure_hashes(root: Path) -> dict[str, str]:
|
|
258
|
+
out: dict[str, str] = {}
|
|
259
|
+
if not root.exists():
|
|
260
|
+
return out
|
|
261
|
+
for path in sorted(root.rglob("*")):
|
|
262
|
+
if not path.is_file():
|
|
263
|
+
continue
|
|
264
|
+
rel = path.relative_to(root)
|
|
265
|
+
parts = rel.parts
|
|
266
|
+
# Mirrors _copy_pure's exclusions: anything not shipped must not be
|
|
267
|
+
# compared either, or the assertion fails on files the bundle drops.
|
|
268
|
+
if "__pycache__" in parts or parts[0] in (".libs", "bin"):
|
|
269
|
+
continue
|
|
270
|
+
if path.suffix == ".so":
|
|
271
|
+
continue
|
|
272
|
+
# dist-info RECORD/WHEEL are per-ABI BY DESIGN (they list the wheel's
|
|
273
|
+
# own files/tags, which include the ABI-specific .so names), so they
|
|
274
|
+
# are exempt from the identity assertion; the canonical ABI's copies
|
|
275
|
+
# ship in the bundle. Everything else in dist-info stays checked.
|
|
276
|
+
if (
|
|
277
|
+
len(parts) >= 2
|
|
278
|
+
and parts[-2].endswith(".dist-info")
|
|
279
|
+
and parts[-1] in ("RECORD", "WHEEL", "direct_url.json")
|
|
280
|
+
):
|
|
281
|
+
continue
|
|
282
|
+
out[rel.as_posix()] = sha256_file(path)
|
|
283
|
+
return out
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _content_stamp(bundle_dir: Path) -> str:
|
|
287
|
+
"""A deterministic stamp over every (relpath, sha256) pair in the bundle."""
|
|
288
|
+
digest = hashlib.sha256()
|
|
289
|
+
for path in sorted(bundle_dir.rglob("*")):
|
|
290
|
+
if not path.is_file():
|
|
291
|
+
continue
|
|
292
|
+
rel = path.relative_to(bundle_dir).as_posix()
|
|
293
|
+
digest.update(rel.encode("utf-8"))
|
|
294
|
+
digest.update(sha256_file(path).encode("ascii"))
|
|
295
|
+
return digest.hexdigest()[:16]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Populate ``jniLibs/<abi>/`` (android/04 §"Native libraries and the .so load model").
|
|
2
|
+
|
|
3
|
+
Two kinds of ``.so``, found two different ways at runtime:
|
|
4
|
+
|
|
5
|
+
- **Shared libraries** resolved by soname (``System.loadLibrary`` /
|
|
6
|
+
``DT_NEEDED``): ``libpython3.X.so``, the runtime's ``lib*_python.so``, the
|
|
7
|
+
SDL family from a wheel's ``.libs/``. Copied verbatim, names unchanged.
|
|
8
|
+
- **Extension modules** imported by dotted name: flattened to
|
|
9
|
+
``libpy.<dotted>.so`` (the platform's native-lib extraction requires the
|
|
10
|
+
``lib*.so`` shape — proven on-device, loadmodel findings §confirmed #1) and
|
|
11
|
+
recorded in the ``ext_manifest.json`` the bootstrap finder consumes.
|
|
12
|
+
|
|
13
|
+
Wheel ``.libs/`` is **flat-only**: a nested subdirectory is a malformed wheel
|
|
14
|
+
and fails the build (android/03/04 — the wheel tag is the sole ABI truth).
|
|
15
|
+
The duplicate policy is the iOS rule adapted: identical content dedupes
|
|
16
|
+
silently; same basename with different bytes aborts naming both providers.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import shutil
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from kivyforge.artifacts.verify import sha256_file
|
|
27
|
+
|
|
28
|
+
EXT_MANIFEST_NAME = "ext_manifest.json"
|
|
29
|
+
|
|
30
|
+
# Suffix pattern of CPython extension modules: everything from the first "."
|
|
31
|
+
# after the module path is the ABI suffix (".cpython-314-....so" or just ".so").
|
|
32
|
+
_SO_SUFFIX = ".so"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class JniLibsError(Exception):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class JniLibsStager:
|
|
41
|
+
"""Accumulates ``.so`` files for one ABI, enforcing the duplicate policy.
|
|
42
|
+
|
|
43
|
+
``provider`` strings name where a file came from (e.g. ``"runtime"``,
|
|
44
|
+
``"wheel kivy"``) so a conflict diagnostic can name both sides.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
dest: Path
|
|
48
|
+
# basename -> (sha256, provider)
|
|
49
|
+
_seen: dict[str, tuple[str, str]] = field(default_factory=dict)
|
|
50
|
+
# dotted module -> flattened filename
|
|
51
|
+
_manifest: dict[str, str] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
def add_shared_library(self, source: Path, *, provider: str) -> None:
|
|
54
|
+
"""Copy a soname-resolved library verbatim (name unchanged)."""
|
|
55
|
+
self._add(source, source.name, provider=provider)
|
|
56
|
+
|
|
57
|
+
def add_extension_module(self, source: Path, *, dotted: str, provider: str) -> None:
|
|
58
|
+
"""Flatten one extension module and record it in the finder manifest."""
|
|
59
|
+
flattened = f"libpy.{dotted}.so"
|
|
60
|
+
existing = self._manifest.get(dotted)
|
|
61
|
+
if existing is not None and existing != flattened:
|
|
62
|
+
raise JniLibsError(
|
|
63
|
+
f"extension module {dotted!r} maps to both {existing!r} and "
|
|
64
|
+
f"{flattened!r}"
|
|
65
|
+
)
|
|
66
|
+
self._add(source, flattened, provider=provider)
|
|
67
|
+
self._manifest[dotted] = flattened
|
|
68
|
+
|
|
69
|
+
def manifest(self) -> dict[str, str]:
|
|
70
|
+
return dict(sorted(self._manifest.items()))
|
|
71
|
+
|
|
72
|
+
def write_manifest(self, bootstrap_dir: Path) -> Path:
|
|
73
|
+
bootstrap_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
out = bootstrap_dir / EXT_MANIFEST_NAME
|
|
75
|
+
out.write_text(
|
|
76
|
+
json.dumps(self.manifest(), indent=1, sort_keys=True) + "\n",
|
|
77
|
+
encoding="utf-8",
|
|
78
|
+
)
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
def _add(self, source: Path, basename: str, *, provider: str) -> None:
|
|
82
|
+
digest = sha256_file(source)
|
|
83
|
+
prior = self._seen.get(basename)
|
|
84
|
+
if prior is not None:
|
|
85
|
+
prior_sha, prior_provider = prior
|
|
86
|
+
if prior_sha == digest:
|
|
87
|
+
return # identical -> silent dedupe
|
|
88
|
+
raise JniLibsError(
|
|
89
|
+
f"conflicting native library {basename!r} for {self.dest.name}: "
|
|
90
|
+
f"{prior_provider} and {provider} supply different contents "
|
|
91
|
+
f"(sha256 {prior_sha[:12]}… vs {digest[:12]}…).\n"
|
|
92
|
+
f" kivyforge never picks a winner — a version-mismatched .so "
|
|
93
|
+
f"can link and then crash at runtime (android/04 §duplicate "
|
|
94
|
+
f".so policy)."
|
|
95
|
+
)
|
|
96
|
+
self.dest.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
shutil.copy2(source, self.dest / basename)
|
|
98
|
+
self._seen[basename] = (digest, provider)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def dotted_module_name(so_path: Path, site_packages_root: Path) -> str:
|
|
102
|
+
"""``site-packages/jnius/jnius.cpython-314-….so`` -> ``jnius.jnius``."""
|
|
103
|
+
rel = so_path.relative_to(site_packages_root)
|
|
104
|
+
stem = rel.name.split(".", 1)[0]
|
|
105
|
+
parts = [*rel.parts[:-1], stem]
|
|
106
|
+
return ".".join(parts)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def stage_wheel_libs_dir(
|
|
110
|
+
stager: JniLibsStager, libs_dir: Path, *, wheel_name: str
|
|
111
|
+
) -> int:
|
|
112
|
+
"""Ingest one wheel's flat ``.libs/`` (SDL family etc.). Returns the count.
|
|
113
|
+
|
|
114
|
+
Flat-only: any subdirectory is a malformed wheel (android/03). The ABI is
|
|
115
|
+
the wheel tag's business — the caller already routed this stager to the
|
|
116
|
+
right ABI.
|
|
117
|
+
"""
|
|
118
|
+
count = 0
|
|
119
|
+
for entry in sorted(libs_dir.iterdir()):
|
|
120
|
+
if entry.is_dir():
|
|
121
|
+
raise JniLibsError(
|
|
122
|
+
f"wheel {wheel_name!r} has a nested .libs/ subdirectory "
|
|
123
|
+
f"({entry.name!r}); .libs/ must be flat — the wheel platform "
|
|
124
|
+
f"tag is the sole source of truth for the ABI (android/03 "
|
|
125
|
+
f"§flat only)."
|
|
126
|
+
)
|
|
127
|
+
if entry.suffix == _SO_SUFFIX:
|
|
128
|
+
stager.add_shared_library(entry, provider=f"wheel {wheel_name}")
|
|
129
|
+
count += 1
|
|
130
|
+
return count
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def stage_site_packages_extensions(
|
|
134
|
+
stager: JniLibsStager, site_packages: Path, *, wheel_name: str
|
|
135
|
+
) -> int:
|
|
136
|
+
"""Flatten every extension ``.so`` under an installed wheel's tree."""
|
|
137
|
+
count = 0
|
|
138
|
+
for so in sorted(site_packages.rglob(f"*{_SO_SUFFIX}")):
|
|
139
|
+
if ".libs" in so.relative_to(site_packages).parts[:1]:
|
|
140
|
+
continue # the flat .libs/ dir is handled by stage_wheel_libs_dir
|
|
141
|
+
stager.add_extension_module(
|
|
142
|
+
so,
|
|
143
|
+
dotted=dotted_module_name(so, site_packages),
|
|
144
|
+
provider=f"wheel {wheel_name}",
|
|
145
|
+
)
|
|
146
|
+
count += 1
|
|
147
|
+
return count
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def stage_runtime_libs(
|
|
151
|
+
stager: JniLibsStager, prefix_lib: Path, *, python_stem: str
|
|
152
|
+
) -> tuple[int, int]:
|
|
153
|
+
"""Stage the extracted runtime's ``prefix/lib`` for one ABI.
|
|
154
|
+
|
|
155
|
+
Returns ``(shared_count, extension_count)``. Follows the python.org
|
|
156
|
+
app-integration split, extended for multi-ABI (android/03 channel 2):
|
|
157
|
+
|
|
158
|
+
- ``lib<python_stem>.so`` + ``lib*_python.so`` -> verbatim (soname libs).
|
|
159
|
+
The runtime's plain ``lib{ssl,crypto,sqlite3}.so`` / ``engines-3`` /
|
|
160
|
+
``ossl-modules`` are NOT shipped — the ``_python``-soname copies are the
|
|
161
|
+
ones the extensions link against (proven on-device; loadmodel findings
|
|
162
|
+
§runtime-package facts).
|
|
163
|
+
- ``lib/<python_stem>/lib-dynload/*.so`` -> flattened extension modules
|
|
164
|
+
(all top-level stdlib names).
|
|
165
|
+
"""
|
|
166
|
+
shared = 0
|
|
167
|
+
libpython = prefix_lib / f"lib{python_stem}.so"
|
|
168
|
+
if not libpython.is_file():
|
|
169
|
+
raise JniLibsError(
|
|
170
|
+
f"runtime prefix has no lib{python_stem}.so under {prefix_lib}"
|
|
171
|
+
)
|
|
172
|
+
stager.add_shared_library(libpython, provider="runtime")
|
|
173
|
+
shared += 1
|
|
174
|
+
for candidate in sorted(prefix_lib.glob("lib*_python.so")):
|
|
175
|
+
stager.add_shared_library(candidate, provider="runtime")
|
|
176
|
+
shared += 1
|
|
177
|
+
|
|
178
|
+
extensions = 0
|
|
179
|
+
dynload = prefix_lib / python_stem / "lib-dynload"
|
|
180
|
+
if not dynload.is_dir():
|
|
181
|
+
raise JniLibsError(f"runtime prefix has no lib-dynload directory at {dynload}")
|
|
182
|
+
for so in sorted(dynload.glob(f"*{_SO_SUFFIX}")):
|
|
183
|
+
dotted = so.name.split(".", 1)[0]
|
|
184
|
+
stager.add_extension_module(so, dotted=dotted, provider="runtime")
|
|
185
|
+
extensions += 1
|
|
186
|
+
return shared, extensions
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Extract the python.org Android runtime per ABI (android/03 channel 2).
|
|
2
|
+
|
|
3
|
+
The tarball layout (loadmodel findings §runtime-package facts):
|
|
4
|
+
``README.md``, ``android-env.sh``, ``android.py``, ``prefix/``, ``testbed/``.
|
|
5
|
+
Only ``prefix/`` matters to the build: ``prefix/lib`` feeds ``jniLibs`` (via
|
|
6
|
+
``stage.jnilibs.stage_runtime_libs``) and ``prefix/lib/pythonX.Y`` (minus
|
|
7
|
+
``lib-dynload``) feeds the asset bundle.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import shutil
|
|
13
|
+
import tarfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RuntimeStageError(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def extract_runtime(tarball: Path, dest: Path) -> Path:
|
|
22
|
+
"""Extract ``tarball`` into ``dest`` and return the ``prefix`` directory.
|
|
23
|
+
|
|
24
|
+
Idempotent per destination: an existing extraction is replaced (the
|
|
25
|
+
tarball's SHA-256 was verified by the collect step before this runs).
|
|
26
|
+
"""
|
|
27
|
+
if dest.exists():
|
|
28
|
+
shutil.rmtree(dest)
|
|
29
|
+
dest.mkdir(parents=True)
|
|
30
|
+
try:
|
|
31
|
+
with tarfile.open(tarball, "r:gz") as tf:
|
|
32
|
+
tf.extractall(dest, filter="data")
|
|
33
|
+
except (tarfile.TarError, OSError) as exc:
|
|
34
|
+
raise RuntimeStageError(
|
|
35
|
+
f"could not extract the python.org runtime {tarball.name}: {exc}"
|
|
36
|
+
) from exc
|
|
37
|
+
prefix = dest / "prefix"
|
|
38
|
+
if not prefix.is_dir():
|
|
39
|
+
# Tolerate a single wrapping directory around the documented layout.
|
|
40
|
+
candidates = [p / "prefix" for p in dest.iterdir() if p.is_dir()]
|
|
41
|
+
matches = [c for c in candidates if c.is_dir()]
|
|
42
|
+
if len(matches) == 1:
|
|
43
|
+
prefix = matches[0]
|
|
44
|
+
else:
|
|
45
|
+
raise RuntimeStageError(
|
|
46
|
+
f"{tarball.name} does not contain the documented prefix/ "
|
|
47
|
+
f"layout (android/03 channel 2); found: "
|
|
48
|
+
f"{[p.name for p in dest.iterdir()]}"
|
|
49
|
+
)
|
|
50
|
+
return prefix
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def stdlib_dir(prefix: Path, python_stem: str) -> Path:
|
|
54
|
+
"""``prefix`` -> the pure-Python stdlib tree (``lib/pythonX.Y``)."""
|
|
55
|
+
lib = prefix / "lib" / python_stem
|
|
56
|
+
if not lib.is_dir():
|
|
57
|
+
raise RuntimeStageError(
|
|
58
|
+
f"runtime prefix has no stdlib at {lib}; the embeddable package "
|
|
59
|
+
f"layout changed — re-lock and report if this persists."
|
|
60
|
+
)
|
|
61
|
+
return lib
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Install locked wheels per ABI (android/06 build step 4).
|
|
2
|
+
|
|
3
|
+
The lock holds the full transitive set with URLs/hashes, so installation is
|
|
4
|
+
``pip install --no-deps --target`` on the **verified local wheel files** — pip
|
|
5
|
+
never re-resolves and never touches an index. Wheel selection per ABI applies
|
|
6
|
+
the tag-floor rule (an ``android_<api>_<abi>`` wheel with ``api <= min_sdk``;
|
|
7
|
+
highest compatible api wins) plus the shared ``py3-none-any`` entries.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from kivyforge.lock.model import LockedPackage, LockedWheel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WheelStageError(Exception):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def select_wheel(package: LockedPackage, *, abi: str, min_sdk: int) -> LockedWheel:
|
|
24
|
+
"""Pick the wheel slice this ABI installs (android/01 §tag floor)."""
|
|
25
|
+
best: tuple[int, LockedWheel] | None = None
|
|
26
|
+
for wheel in package.wheels:
|
|
27
|
+
if wheel.is_pure_python:
|
|
28
|
+
if best is None:
|
|
29
|
+
best = (-1, wheel)
|
|
30
|
+
continue
|
|
31
|
+
tag = wheel.platform_tag
|
|
32
|
+
parts = tag.split("_", 2)
|
|
33
|
+
if parts[0] != "android" or len(parts) != 3 or parts[2] != abi:
|
|
34
|
+
continue
|
|
35
|
+
try:
|
|
36
|
+
api = int(parts[1])
|
|
37
|
+
except ValueError:
|
|
38
|
+
continue
|
|
39
|
+
if api <= min_sdk and (best is None or api > best[0]):
|
|
40
|
+
best = (api, wheel)
|
|
41
|
+
if best is None:
|
|
42
|
+
raise WheelStageError(
|
|
43
|
+
f"{package.name} {package.version} has no wheel for ABI {abi!r} "
|
|
44
|
+
f"(min_sdk {min_sdk}) in the lock — re-run `kivyforge lock`."
|
|
45
|
+
)
|
|
46
|
+
return best[1]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def install_wheels(
|
|
50
|
+
wheel_files: list[Path],
|
|
51
|
+
target: Path,
|
|
52
|
+
*,
|
|
53
|
+
python_version: str,
|
|
54
|
+
python_executable: str | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""``pip install --no-deps --target`` the verified wheel files.
|
|
57
|
+
|
|
58
|
+
Cross-install: the host interpreter rarely matches the target's version or
|
|
59
|
+
platform, so the target environment is described explicitly — the platform
|
|
60
|
+
tags are taken from the selected wheel files themselves (exact match by
|
|
61
|
+
construction), the version/ABI from the locked target Python.
|
|
62
|
+
"""
|
|
63
|
+
if not wheel_files:
|
|
64
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
return
|
|
66
|
+
from kivyforge.lock.resolver import abi_tags, pip_python_version
|
|
67
|
+
|
|
68
|
+
platforms: list[str] = []
|
|
69
|
+
for wheel in wheel_files:
|
|
70
|
+
stem = wheel.name[:-4] if wheel.name.endswith(".whl") else wheel.name
|
|
71
|
+
tag = stem.rsplit("-", 1)[-1]
|
|
72
|
+
if tag != "any" and tag not in platforms:
|
|
73
|
+
platforms.append(tag)
|
|
74
|
+
|
|
75
|
+
cmd = [
|
|
76
|
+
python_executable or sys.executable,
|
|
77
|
+
"-m",
|
|
78
|
+
"pip",
|
|
79
|
+
"install",
|
|
80
|
+
"--no-deps",
|
|
81
|
+
"--no-index",
|
|
82
|
+
"--only-binary=:all:",
|
|
83
|
+
"--python-version",
|
|
84
|
+
pip_python_version(python_version),
|
|
85
|
+
"--implementation",
|
|
86
|
+
"cp",
|
|
87
|
+
"--target",
|
|
88
|
+
str(target),
|
|
89
|
+
"--quiet",
|
|
90
|
+
]
|
|
91
|
+
for platform_tag in platforms:
|
|
92
|
+
cmd += ["--platform", platform_tag]
|
|
93
|
+
for abi in abi_tags(python_version):
|
|
94
|
+
cmd += ["--abi", abi]
|
|
95
|
+
cmd += [str(w) for w in wheel_files]
|
|
96
|
+
|
|
97
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
98
|
+
if proc.returncode != 0:
|
|
99
|
+
raise WheelStageError(
|
|
100
|
+
"pip could not install the locked wheels into the staging "
|
|
101
|
+
f"directory.\n pip said:\n{_indent(proc.stderr or proc.stdout)}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _indent(text: str) -> str:
|
|
106
|
+
return "\n".join(f" {line}" for line in (text or "").splitlines())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""kivyforge-emitted Android toolchain pins (android/06 §versioning, layer 2).
|
|
2
|
+
|
|
3
|
+
These move with a kivyforge release and are validated together — the exact
|
|
4
|
+
combination the Phase-0 prototype proved on-device
|
|
5
|
+
(docs/design/dev/android-loadmodel-findings.md §runtime-package facts). The
|
|
6
|
+
generated project is regenerated on every build, so bumping kivyforge and
|
|
7
|
+
rebuilding is how projects move across these pins.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# Gradle wrapper distribution the generated project pins (the python.org
|
|
13
|
+
# testbed's own combo, proven with AGP below).
|
|
14
|
+
GRADLE_VERSION = "8.11.1"
|
|
15
|
+
GRADLE_DISTRIBUTION_URL = (
|
|
16
|
+
f"https\\://services.gradle.org/distributions/gradle-{GRADLE_VERSION}-bin.zip"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Android Gradle Plugin version emitted into the root build.gradle.
|
|
20
|
+
AGP_VERSION = "8.10.0"
|
|
21
|
+
|
|
22
|
+
# Kotlin plugin version (applied only when [tool.kivy.android.src].kotlin is
|
|
23
|
+
# non-empty).
|
|
24
|
+
KOTLIN_VERSION = "1.9.22"
|
|
25
|
+
|
|
26
|
+
# ndkVersion emitted into app/build.gradle: the launcher compile is
|
|
27
|
+
# deterministic across hosts (android/04 §native launcher). r27.3 is the
|
|
28
|
+
# proven prototype pin; cibuildwheel's wheels pin the same family.
|
|
29
|
+
NDK_VERSION = "27.3.13750724"
|