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,899 @@
|
|
|
1
|
+
"""``kivyforge doctor`` — Android checks (android/06 §doctor).
|
|
2
|
+
|
|
3
|
+
Environment checks (JDK, SDK, build-tools, NDK, emulator, adb) touch the host
|
|
4
|
+
only through an injectable ``AndroidProbe`` so the check logic is unit-testable
|
|
5
|
+
with a fake. Project checks assert against real seams (the config, the lock,
|
|
6
|
+
staged ``.so``s, the generated manifest) and are exercised on a real project.
|
|
7
|
+
|
|
8
|
+
kivyforge never installs the host toolchain (android/06): every check only
|
|
9
|
+
*detects* and prints an actionable hint derived from the project's own config.
|
|
10
|
+
Each check reports PASS / WARN / FAIL / SKIP; exit is non-zero only on FAIL.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Protocol
|
|
19
|
+
|
|
20
|
+
from kivyforge.config.errors import ConfigError
|
|
21
|
+
from kivyforge.config.loader import load_config
|
|
22
|
+
from kivyforge.config.model import AndroidConfig, Config
|
|
23
|
+
from kivyforge.doctor.result import CheckResult, Status
|
|
24
|
+
|
|
25
|
+
from . import toolchain
|
|
26
|
+
from .bootstrap.contract import (
|
|
27
|
+
COMPATIBLE_PYJNIUS,
|
|
28
|
+
ContractError,
|
|
29
|
+
check_pyjnius_contract,
|
|
30
|
+
)
|
|
31
|
+
from .elf import scan_alignment
|
|
32
|
+
from .lock import reader as lock_reader
|
|
33
|
+
from .splash import SPLASH_MIN_COMPILE_SDK
|
|
34
|
+
|
|
35
|
+
_EXE = ".exe" if os.name == "nt" else ""
|
|
36
|
+
_BAT = ".bat" if os.name == "nt" else ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AndroidProbe(Protocol):
|
|
40
|
+
def which(self, name: str) -> str | None: ...
|
|
41
|
+
def java_home(self) -> str | None: ...
|
|
42
|
+
def sdk_root(self) -> Path | None: ...
|
|
43
|
+
def ndk_versions(self, sdk: Path) -> list[str]: ...
|
|
44
|
+
def build_tools_versions(self, sdk: Path) -> list[str]: ...
|
|
45
|
+
def platform_installed(self, sdk: Path, api: int) -> bool: ...
|
|
46
|
+
def accepted_licenses(self, sdk: Path) -> list[str]: ...
|
|
47
|
+
def avds(self) -> list[str]: ...
|
|
48
|
+
def connected_devices(self) -> list[str]: ...
|
|
49
|
+
def has_kvm_or_haxm(self) -> bool: ...
|
|
50
|
+
def tcp_reachable(self, host: str, port: int) -> bool: ...
|
|
51
|
+
def latest_kivyforge_version(self) -> str | None: ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RealAndroidProbe:
|
|
55
|
+
def which(self, name: str) -> str | None:
|
|
56
|
+
return shutil.which(name)
|
|
57
|
+
|
|
58
|
+
def java_home(self) -> str | None:
|
|
59
|
+
jh = os.environ.get("JAVA_HOME")
|
|
60
|
+
if jh and (Path(jh) / "bin" / f"java{_EXE}").exists():
|
|
61
|
+
return jh
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def sdk_root(self) -> Path | None:
|
|
65
|
+
for var in ("ANDROID_HOME", "ANDROID_SDK_ROOT"):
|
|
66
|
+
value = os.environ.get(var)
|
|
67
|
+
if value and Path(value).is_dir():
|
|
68
|
+
return Path(value)
|
|
69
|
+
candidates = []
|
|
70
|
+
if os.name == "nt":
|
|
71
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
72
|
+
if local:
|
|
73
|
+
candidates.append(Path(local) / "Android" / "Sdk")
|
|
74
|
+
else:
|
|
75
|
+
home = Path.home()
|
|
76
|
+
candidates += [home / "Android" / "Sdk", home / "android-sdk"]
|
|
77
|
+
return next((c for c in candidates if c.is_dir()), None)
|
|
78
|
+
|
|
79
|
+
def ndk_versions(self, sdk: Path) -> list[str]:
|
|
80
|
+
root = sdk / "ndk"
|
|
81
|
+
return (
|
|
82
|
+
sorted(p.name for p in root.iterdir() if p.is_dir())
|
|
83
|
+
if root.is_dir()
|
|
84
|
+
else []
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def build_tools_versions(self, sdk: Path) -> list[str]:
|
|
88
|
+
root = sdk / "build-tools"
|
|
89
|
+
return (
|
|
90
|
+
sorted(p.name for p in root.iterdir() if p.is_dir())
|
|
91
|
+
if root.is_dir()
|
|
92
|
+
else []
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def platform_installed(self, sdk: Path, api: int) -> bool:
|
|
96
|
+
return (sdk / "platforms" / f"android-{api}").is_dir()
|
|
97
|
+
|
|
98
|
+
def accepted_licenses(self, sdk: Path) -> list[str]:
|
|
99
|
+
"""The license hash files ``sdkmanager --licenses`` writes on accept."""
|
|
100
|
+
root = sdk / "licenses"
|
|
101
|
+
return (
|
|
102
|
+
sorted(p.name for p in root.iterdir() if p.is_file())
|
|
103
|
+
if root.is_dir()
|
|
104
|
+
else []
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def avds(self) -> list[str]:
|
|
108
|
+
try:
|
|
109
|
+
from .adb import available_avds
|
|
110
|
+
|
|
111
|
+
return available_avds()
|
|
112
|
+
except Exception: # noqa: BLE001
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
def connected_devices(self) -> list[str]:
|
|
116
|
+
try:
|
|
117
|
+
from .adb import connected_devices
|
|
118
|
+
|
|
119
|
+
return connected_devices()
|
|
120
|
+
except Exception: # noqa: BLE001
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
def latest_kivyforge_version(self) -> str | None:
|
|
124
|
+
from kivyforge.doctor.probe import RealProbe
|
|
125
|
+
|
|
126
|
+
return RealProbe().latest_kivyforge_version()
|
|
127
|
+
|
|
128
|
+
def has_kvm_or_haxm(self) -> bool:
|
|
129
|
+
if os.name == "nt":
|
|
130
|
+
return True # WHPX/HAXM presence is hard to probe cheaply; assume ok
|
|
131
|
+
return Path("/dev/kvm").exists()
|
|
132
|
+
|
|
133
|
+
def tcp_reachable(self, host: str, port: int) -> bool:
|
|
134
|
+
import socket
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
with socket.create_connection((host, port), timeout=5):
|
|
138
|
+
return True
|
|
139
|
+
except OSError:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# --------------------------------------------------------------------------- #
|
|
144
|
+
# environment checks
|
|
145
|
+
# --------------------------------------------------------------------------- #
|
|
146
|
+
def _check_kivyforge_version(
|
|
147
|
+
probe: AndroidProbe, current: str, *, offline: bool
|
|
148
|
+
) -> CheckResult:
|
|
149
|
+
"""Report the running kivyforge, and nudge when PyPI has a newer one.
|
|
150
|
+
|
|
151
|
+
The generated project's toolchain pins (AGP, Gradle, NDK) move with a
|
|
152
|
+
kivyforge release, so "am I current" is a build-relevant question here, not
|
|
153
|
+
just housekeeping.
|
|
154
|
+
"""
|
|
155
|
+
if offline:
|
|
156
|
+
return CheckResult("kivyforge", Status.PASS, f"{current} (offline)")
|
|
157
|
+
from kivyforge.doctor.checks_common import _ver_tuple
|
|
158
|
+
|
|
159
|
+
latest = probe.latest_kivyforge_version()
|
|
160
|
+
if latest and _ver_tuple(latest) > _ver_tuple(current):
|
|
161
|
+
return CheckResult(
|
|
162
|
+
"kivyforge",
|
|
163
|
+
Status.WARN,
|
|
164
|
+
f"{current} (latest {latest})",
|
|
165
|
+
hint="upgrade with `pip install -U kivyforge`.",
|
|
166
|
+
)
|
|
167
|
+
return CheckResult("kivyforge", Status.PASS, current)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _check_jdk(probe: AndroidProbe) -> CheckResult:
|
|
171
|
+
jh = probe.java_home()
|
|
172
|
+
if jh:
|
|
173
|
+
return CheckResult("JDK", Status.PASS, f"JAVA_HOME={jh}")
|
|
174
|
+
java = probe.which("java")
|
|
175
|
+
if java:
|
|
176
|
+
return CheckResult("JDK", Status.PASS, java)
|
|
177
|
+
return CheckResult(
|
|
178
|
+
"JDK",
|
|
179
|
+
Status.FAIL,
|
|
180
|
+
"no java on PATH and JAVA_HOME unset",
|
|
181
|
+
hint="install a JDK 17+ (AGP requires it) and set JAVA_HOME.",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _check_sdk(sdk: Path | None) -> CheckResult:
|
|
186
|
+
if sdk is None:
|
|
187
|
+
return CheckResult(
|
|
188
|
+
"Android SDK",
|
|
189
|
+
Status.FAIL,
|
|
190
|
+
"ANDROID_HOME/ANDROID_SDK_ROOT unset and no SDK at the conventional "
|
|
191
|
+
"location",
|
|
192
|
+
hint="install the SDK and set ANDROID_HOME.",
|
|
193
|
+
)
|
|
194
|
+
sdkmanager = sdk / "cmdline-tools" / "latest" / "bin" / f"sdkmanager{_BAT}"
|
|
195
|
+
if not sdkmanager.exists():
|
|
196
|
+
return CheckResult(
|
|
197
|
+
"Android SDK",
|
|
198
|
+
Status.WARN,
|
|
199
|
+
f"{sdk} exists but cmdline-tools/latest is missing",
|
|
200
|
+
hint="install the 'cmdline-tools;latest' package.",
|
|
201
|
+
)
|
|
202
|
+
return CheckResult("Android SDK", Status.PASS, str(sdk))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _check_build_tools(
|
|
206
|
+
probe: AndroidProbe, sdk: Path | None, android: AndroidConfig | None
|
|
207
|
+
) -> CheckResult:
|
|
208
|
+
if sdk is None:
|
|
209
|
+
return CheckResult("Build-tools / platform", Status.SKIP, "no SDK root")
|
|
210
|
+
versions = probe.build_tools_versions(sdk)
|
|
211
|
+
if not versions:
|
|
212
|
+
return CheckResult(
|
|
213
|
+
"Build-tools / platform",
|
|
214
|
+
Status.FAIL,
|
|
215
|
+
"no build-tools installed",
|
|
216
|
+
hint="sdkmanager 'build-tools;<version>'.",
|
|
217
|
+
)
|
|
218
|
+
if android is not None and not probe.platform_installed(sdk, android.compile_sdk):
|
|
219
|
+
return CheckResult(
|
|
220
|
+
"Build-tools / platform",
|
|
221
|
+
Status.FAIL,
|
|
222
|
+
f"the compile_sdk platform (android-{android.compile_sdk}) is not "
|
|
223
|
+
"installed",
|
|
224
|
+
hint=f"sdkmanager 'platforms;android-{android.compile_sdk}'.",
|
|
225
|
+
)
|
|
226
|
+
return CheckResult(
|
|
227
|
+
"Build-tools / platform", Status.PASS, f"build-tools {versions[-1]}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _check_ndk(probe: AndroidProbe, sdk: Path | None) -> CheckResult:
|
|
232
|
+
if sdk is None:
|
|
233
|
+
return CheckResult("NDK", Status.SKIP, "no SDK root to look in")
|
|
234
|
+
versions = probe.ndk_versions(sdk)
|
|
235
|
+
if not versions:
|
|
236
|
+
return CheckResult(
|
|
237
|
+
"NDK",
|
|
238
|
+
Status.FAIL,
|
|
239
|
+
"no NDK installed (required for EVERY build — it compiles the native "
|
|
240
|
+
"launcher)",
|
|
241
|
+
hint="sdkmanager 'ndk;<version>' (r27+ recommended for 16 KB).",
|
|
242
|
+
)
|
|
243
|
+
return CheckResult("NDK", Status.PASS, ", ".join(versions))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _check_sdk_licenses(probe: AndroidProbe, sdk: Path | None) -> CheckResult:
|
|
247
|
+
"""Unaccepted SDK licenses stop AGP mid-build with an opaque message.
|
|
248
|
+
|
|
249
|
+
Acceptance is recorded as hash files under ``<sdk>/licenses/``; that is what
|
|
250
|
+
AGP's own auto-download consults, so their presence is the whole signal.
|
|
251
|
+
"""
|
|
252
|
+
if sdk is None:
|
|
253
|
+
return CheckResult("SDK licenses", Status.SKIP, "no SDK root")
|
|
254
|
+
accepted = probe.accepted_licenses(sdk)
|
|
255
|
+
if not accepted:
|
|
256
|
+
return CheckResult(
|
|
257
|
+
"SDK licenses",
|
|
258
|
+
Status.WARN,
|
|
259
|
+
f"no accepted licenses recorded under {sdk / 'licenses'}",
|
|
260
|
+
hint="run `sdkmanager --licenses` and accept; otherwise Gradle "
|
|
261
|
+
"refuses to auto-install the SDK packages a build needs.",
|
|
262
|
+
)
|
|
263
|
+
return CheckResult("SDK licenses", Status.PASS, f"{len(accepted)} accepted")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _check_gradle_wrapper(project_dir: Path) -> CheckResult:
|
|
267
|
+
"""The generated project builds through its own pinned wrapper (android/06).
|
|
268
|
+
|
|
269
|
+
Not the host's ``gradle``: the wrapper is what makes the Gradle version part
|
|
270
|
+
of kivyforge's toolchain pins, so a hand-edited or missing one is the
|
|
271
|
+
difference between a reproducible build and whatever the host has.
|
|
272
|
+
"""
|
|
273
|
+
if not project_dir.is_dir():
|
|
274
|
+
return CheckResult(
|
|
275
|
+
"Gradle wrapper", Status.SKIP, "project not generated yet (run build)"
|
|
276
|
+
)
|
|
277
|
+
script = project_dir / ("gradlew.bat" if os.name == "nt" else "gradlew")
|
|
278
|
+
properties = project_dir / "gradle" / "wrapper" / "gradle-wrapper.properties"
|
|
279
|
+
missing = [p.name for p in (script, properties) if not p.is_file()]
|
|
280
|
+
if missing:
|
|
281
|
+
return CheckResult(
|
|
282
|
+
"Gradle wrapper",
|
|
283
|
+
Status.FAIL,
|
|
284
|
+
f"missing {', '.join(missing)}",
|
|
285
|
+
hint="re-run `kivyforge build -p android` to regenerate the project.",
|
|
286
|
+
)
|
|
287
|
+
text = properties.read_text(encoding="utf-8")
|
|
288
|
+
if toolchain.GRADLE_VERSION not in text:
|
|
289
|
+
return CheckResult(
|
|
290
|
+
"Gradle wrapper",
|
|
291
|
+
Status.WARN,
|
|
292
|
+
f"distributionUrl is not the pinned Gradle {toolchain.GRADLE_VERSION}",
|
|
293
|
+
hint="the generated project is a managed artifact; rebuild rather "
|
|
294
|
+
"than editing it (a hand-edited wrapper is overwritten anyway).",
|
|
295
|
+
)
|
|
296
|
+
return CheckResult(
|
|
297
|
+
"Gradle wrapper", Status.PASS, f"gradle {toolchain.GRADLE_VERSION}"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _check_adb(probe: AndroidProbe, sdk: Path | None) -> CheckResult:
|
|
302
|
+
adb = probe.which("adb")
|
|
303
|
+
if adb is None and sdk is not None:
|
|
304
|
+
candidate = sdk / "platform-tools" / f"adb{_EXE}"
|
|
305
|
+
if candidate.exists():
|
|
306
|
+
adb = str(candidate)
|
|
307
|
+
if adb is None:
|
|
308
|
+
return CheckResult(
|
|
309
|
+
"adb",
|
|
310
|
+
Status.WARN,
|
|
311
|
+
"platform-tools not found",
|
|
312
|
+
hint="sdkmanager platform-tools (needed for `kivyforge run`).",
|
|
313
|
+
)
|
|
314
|
+
devices = probe.connected_devices()
|
|
315
|
+
if not devices:
|
|
316
|
+
return CheckResult(
|
|
317
|
+
"adb",
|
|
318
|
+
Status.WARN,
|
|
319
|
+
f"{adb}; no device or emulator attached",
|
|
320
|
+
hint="attach a device (USB debugging on) or boot an AVD; "
|
|
321
|
+
"`kivyforge run` and `run --smoke` need one.",
|
|
322
|
+
)
|
|
323
|
+
return CheckResult("adb", Status.PASS, f"{adb}; {', '.join(devices)}")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _check_emulator(probe: AndroidProbe, sdk: Path | None) -> CheckResult:
|
|
327
|
+
avds = probe.avds()
|
|
328
|
+
accel = probe.has_kvm_or_haxm()
|
|
329
|
+
if not avds:
|
|
330
|
+
return CheckResult(
|
|
331
|
+
"Emulator / virtualization",
|
|
332
|
+
Status.WARN,
|
|
333
|
+
"no AVD found",
|
|
334
|
+
hint="create an AVD (Android Studio Device Manager or avdmanager) "
|
|
335
|
+
"for `kivyforge run --emulator`.",
|
|
336
|
+
)
|
|
337
|
+
if not accel:
|
|
338
|
+
return CheckResult(
|
|
339
|
+
"Emulator / virtualization",
|
|
340
|
+
Status.WARN,
|
|
341
|
+
f"AVDs present ({len(avds)}) but no hardware acceleration",
|
|
342
|
+
hint="enable KVM (Linux) / HAXM or Hyper-V (macOS/Windows).",
|
|
343
|
+
)
|
|
344
|
+
return CheckResult(
|
|
345
|
+
"Emulator / virtualization", Status.PASS, f"{len(avds)} AVD(s), accelerated"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# --------------------------------------------------------------------------- #
|
|
350
|
+
# project checks
|
|
351
|
+
# --------------------------------------------------------------------------- #
|
|
352
|
+
def _check_sdl_kivy_match(config: Config, lock) -> CheckResult:
|
|
353
|
+
android = config.android_required
|
|
354
|
+
kivy = next((p for p in lock.packages if p.name.lower() == "kivy"), None)
|
|
355
|
+
if kivy is None:
|
|
356
|
+
return CheckResult("SDL / Kivy match", Status.SKIP, "kivy not in the lock")
|
|
357
|
+
from packaging.version import InvalidVersion, Version
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
# ``.major`` reads only the release segment's leading number, so a
|
|
361
|
+
# pre-release like "3.0.0.dev202606221936" still counts as major 3 --
|
|
362
|
+
# a direct ``Version(...) >= Version("3.0")`` comparison would not:
|
|
363
|
+
# PEP 440 dev-releases sort *before* their final release, so that
|
|
364
|
+
# comparison is False for every Kivy 3.0 dev build.
|
|
365
|
+
is_sdl3 = Version(kivy.version).major >= 3
|
|
366
|
+
except InvalidVersion:
|
|
367
|
+
return CheckResult(
|
|
368
|
+
"SDL / Kivy match", Status.WARN, f"unparseable kivy {kivy.version!r}"
|
|
369
|
+
)
|
|
370
|
+
expected = 3 if is_sdl3 else 2
|
|
371
|
+
if android.kivy_generation != expected:
|
|
372
|
+
return CheckResult(
|
|
373
|
+
"SDL / Kivy match",
|
|
374
|
+
Status.WARN,
|
|
375
|
+
f"kivy_generation = {android.kivy_generation} but kivy {kivy.version} "
|
|
376
|
+
f"is SDL{expected}",
|
|
377
|
+
hint=f"set kivy_generation = {expected} and re-lock.",
|
|
378
|
+
)
|
|
379
|
+
return CheckResult(
|
|
380
|
+
"SDL / Kivy match",
|
|
381
|
+
Status.PASS,
|
|
382
|
+
f"kivy {kivy.version} / kivy_generation {android.kivy_generation}",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _check_pyjnius_contract(lock) -> CheckResult:
|
|
387
|
+
pyjnius = next((p for p in lock.packages if p.name.lower() == "pyjnius"), None)
|
|
388
|
+
if pyjnius is None:
|
|
389
|
+
return CheckResult(
|
|
390
|
+
"pyjnius / bootstrap match", Status.SKIP, "pyjnius not in the lock"
|
|
391
|
+
)
|
|
392
|
+
try:
|
|
393
|
+
check_pyjnius_contract(pyjnius.version)
|
|
394
|
+
except ContractError:
|
|
395
|
+
return CheckResult(
|
|
396
|
+
"pyjnius / bootstrap match",
|
|
397
|
+
Status.FAIL,
|
|
398
|
+
f"locked pyjnius {pyjnius.version} is outside the bootstrap template's "
|
|
399
|
+
f"invoke0 range ({COMPATIBLE_PYJNIUS})",
|
|
400
|
+
hint="re-lock to a compatible pyjnius or upgrade kivyforge.",
|
|
401
|
+
)
|
|
402
|
+
return CheckResult(
|
|
403
|
+
"pyjnius / bootstrap match",
|
|
404
|
+
Status.PASS,
|
|
405
|
+
f"pyjnius {pyjnius.version} in range",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _check_16k_alignment(project_dir: Path) -> CheckResult:
|
|
410
|
+
jnilibs = project_dir / "app" / "src" / "main" / "jniLibs"
|
|
411
|
+
if not jnilibs.is_dir():
|
|
412
|
+
return CheckResult(
|
|
413
|
+
"16 KB alignment", Status.SKIP, "no staged jniLibs yet (run build)"
|
|
414
|
+
)
|
|
415
|
+
bad = scan_alignment(jnilibs)
|
|
416
|
+
if bad:
|
|
417
|
+
names = ", ".join(f"{p.name} (0x{a:x})" for p, a in bad[:5])
|
|
418
|
+
return CheckResult(
|
|
419
|
+
"16 KB alignment",
|
|
420
|
+
Status.FAIL,
|
|
421
|
+
f"{len(bad)} native lib(s) below 16 KB LOAD alignment: {names}",
|
|
422
|
+
hint="rebuild the offending wheel with NDK r28+ or "
|
|
423
|
+
"-Wl,-z,max-page-size=16384 (android/03 §wheel content rules).",
|
|
424
|
+
)
|
|
425
|
+
return CheckResult(
|
|
426
|
+
"16 KB alignment", Status.PASS, "all staged .so's are 16 KB-aligned"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _check_abi_coverage(config: Config, lock) -> CheckResult:
|
|
431
|
+
android = config.android_required
|
|
432
|
+
from kivyforge.platforms.android.stage.wheels import (
|
|
433
|
+
WheelStageError,
|
|
434
|
+
select_wheel,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
missing = []
|
|
438
|
+
for package in lock.packages:
|
|
439
|
+
for abi in android.abis:
|
|
440
|
+
try:
|
|
441
|
+
select_wheel(package, abi=abi, min_sdk=android.min_sdk)
|
|
442
|
+
except WheelStageError:
|
|
443
|
+
missing.append(f"{package.name}/{abi}")
|
|
444
|
+
if missing:
|
|
445
|
+
return CheckResult(
|
|
446
|
+
"ABI coverage",
|
|
447
|
+
Status.FAIL,
|
|
448
|
+
f"missing wheel slices: {', '.join(missing[:5])}",
|
|
449
|
+
hint="re-lock, or narrow [tool.kivy.android].abis.",
|
|
450
|
+
)
|
|
451
|
+
return CheckResult(
|
|
452
|
+
"ABI coverage", Status.PASS, f"every package covers {', '.join(android.abis)}"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _check_signing(config: Config, project_root: Path) -> CheckResult:
|
|
457
|
+
android = config.android_required
|
|
458
|
+
if not android.signing.configured:
|
|
459
|
+
return CheckResult(
|
|
460
|
+
"Signing (release)",
|
|
461
|
+
Status.SKIP,
|
|
462
|
+
"[tool.kivy.android.signing] not configured (debug builds are fine)",
|
|
463
|
+
)
|
|
464
|
+
from .signing import SigningError, resolve_signing
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
resolve_signing(android.signing, project_root=project_root)
|
|
468
|
+
except SigningError as exc:
|
|
469
|
+
return CheckResult(
|
|
470
|
+
"Signing (release)",
|
|
471
|
+
Status.FAIL,
|
|
472
|
+
str(exc).splitlines()[0],
|
|
473
|
+
hint="see `kivyforge package` signing prerequisites (android/07).",
|
|
474
|
+
)
|
|
475
|
+
return CheckResult("Signing (release)", Status.PASS, "keystore + alias resolve")
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _check_icon(config: Config, project_root: Path) -> CheckResult:
|
|
479
|
+
from kivyforge.config.icons import icon_source_problem
|
|
480
|
+
|
|
481
|
+
icons = config.android_required.icons
|
|
482
|
+
if not icons.source:
|
|
483
|
+
return CheckResult(
|
|
484
|
+
"App icon",
|
|
485
|
+
Status.SKIP,
|
|
486
|
+
"no [tool.kivy.android.icons].source (default icon)",
|
|
487
|
+
)
|
|
488
|
+
problem = icon_source_problem(project_root / icons.source)
|
|
489
|
+
if problem:
|
|
490
|
+
return CheckResult(
|
|
491
|
+
"App icon",
|
|
492
|
+
Status.FAIL,
|
|
493
|
+
problem.splitlines()[0],
|
|
494
|
+
hint="a bad icon source fails the AAPT run, not the config load.",
|
|
495
|
+
)
|
|
496
|
+
# The layers are only referenced when set; a hex background is not a path.
|
|
497
|
+
for key, value in (
|
|
498
|
+
("background", icons.background),
|
|
499
|
+
("monochrome", icons.monochrome),
|
|
500
|
+
):
|
|
501
|
+
if value and not value.startswith("#") and not (project_root / value).is_file():
|
|
502
|
+
return CheckResult(
|
|
503
|
+
"App icon",
|
|
504
|
+
Status.FAIL,
|
|
505
|
+
f"[tool.kivy.android.icons].{key} not found: {value}",
|
|
506
|
+
)
|
|
507
|
+
return CheckResult("App icon", Status.PASS, icons.source)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _check_splash(config: Config, project_root: Path) -> CheckResult:
|
|
511
|
+
"""Catch a bad splash path here rather than at AAPT time (android/06)."""
|
|
512
|
+
from kivyforge.config.icons import IconSourceError, png_dimensions
|
|
513
|
+
|
|
514
|
+
android = config.android_required
|
|
515
|
+
splash = android.splash
|
|
516
|
+
if not splash.source:
|
|
517
|
+
return CheckResult(
|
|
518
|
+
"Splash assets", Status.SKIP, "no [tool.kivy.android.splash].source"
|
|
519
|
+
)
|
|
520
|
+
source = project_root / splash.source
|
|
521
|
+
if not source.is_file():
|
|
522
|
+
return CheckResult(
|
|
523
|
+
"Splash assets",
|
|
524
|
+
Status.FAIL,
|
|
525
|
+
f"[tool.kivy.android.splash].source not found: {source}",
|
|
526
|
+
hint="expected a PNG, or an AnimatedVectorDrawable XML.",
|
|
527
|
+
)
|
|
528
|
+
animated = source.suffix.lower() == ".xml"
|
|
529
|
+
if animated:
|
|
530
|
+
text = source.read_text(encoding="utf-8", errors="replace")
|
|
531
|
+
if "<vector" not in text and "<animated-vector" not in text:
|
|
532
|
+
return CheckResult(
|
|
533
|
+
"Splash assets",
|
|
534
|
+
Status.FAIL,
|
|
535
|
+
f"{splash.source} is XML but is neither a <vector> nor an "
|
|
536
|
+
"<animated-vector> drawable",
|
|
537
|
+
)
|
|
538
|
+
animated = "<animated-vector" in text
|
|
539
|
+
else:
|
|
540
|
+
try:
|
|
541
|
+
png_dimensions(source)
|
|
542
|
+
except (IconSourceError, OSError) as exc:
|
|
543
|
+
return CheckResult("Splash assets", Status.FAIL, str(exc).splitlines()[0])
|
|
544
|
+
if splash.branding and not (project_root / splash.branding).is_file():
|
|
545
|
+
return CheckResult(
|
|
546
|
+
"Splash assets",
|
|
547
|
+
Status.FAIL,
|
|
548
|
+
f"[tool.kivy.android.splash].branding not found: {splash.branding}",
|
|
549
|
+
)
|
|
550
|
+
if android.compile_sdk < SPLASH_MIN_COMPILE_SDK:
|
|
551
|
+
return CheckResult(
|
|
552
|
+
"Splash assets",
|
|
553
|
+
Status.FAIL,
|
|
554
|
+
f"windowSplashScreen* need compile_sdk >= {SPLASH_MIN_COMPILE_SDK}; "
|
|
555
|
+
f"this project compiles against android-{android.compile_sdk}",
|
|
556
|
+
hint="raise [tool.kivy.android].compile_sdk.",
|
|
557
|
+
)
|
|
558
|
+
if splash.animation_duration is not None and not animated:
|
|
559
|
+
return CheckResult(
|
|
560
|
+
"Splash assets",
|
|
561
|
+
Status.WARN,
|
|
562
|
+
f"animation_duration is set but {splash.source} is not an "
|
|
563
|
+
"AnimatedVectorDrawable; it will be ignored",
|
|
564
|
+
)
|
|
565
|
+
return CheckResult("Splash assets", Status.PASS, splash.source)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _check_include_files(config: Config, project_root: Path, lock) -> CheckResult:
|
|
569
|
+
"""Report include_files drift as a warning before the build refuses it."""
|
|
570
|
+
from kivyforge.artifacts.verify import sha256_file
|
|
571
|
+
|
|
572
|
+
entries = config.android_required.include_files
|
|
573
|
+
if not entries:
|
|
574
|
+
return CheckResult(
|
|
575
|
+
"include_files", Status.SKIP, "no [[tool.kivy.android.include_files]]"
|
|
576
|
+
)
|
|
577
|
+
pins = {(p.dest, p.source): p.sha256 for p in lock.include_files}
|
|
578
|
+
staged: set[tuple[str, str]] = set()
|
|
579
|
+
drifted: list[str] = []
|
|
580
|
+
for entry in entries:
|
|
581
|
+
for source in entry.sources:
|
|
582
|
+
local = project_root / source
|
|
583
|
+
children = (
|
|
584
|
+
[c for c in sorted(local.rglob("*")) if c.is_file()]
|
|
585
|
+
if local.is_dir()
|
|
586
|
+
else [local]
|
|
587
|
+
)
|
|
588
|
+
for child in children:
|
|
589
|
+
rel = (
|
|
590
|
+
(Path(source) / child.relative_to(local)).as_posix()
|
|
591
|
+
if local.is_dir()
|
|
592
|
+
else source
|
|
593
|
+
)
|
|
594
|
+
staged.add((entry.dest, rel))
|
|
595
|
+
recorded = pins.get((entry.dest, rel))
|
|
596
|
+
if not child.is_file():
|
|
597
|
+
drifted.append(f"{rel} (missing)")
|
|
598
|
+
elif recorded is None:
|
|
599
|
+
drifted.append(f"{rel} (not in the lock)")
|
|
600
|
+
elif recorded != sha256_file(child):
|
|
601
|
+
drifted.append(f"{rel} (changed)")
|
|
602
|
+
drifted += [f"{source} (deleted)" for _dest, source in sorted(pins.keys() - staged)]
|
|
603
|
+
if drifted:
|
|
604
|
+
return CheckResult(
|
|
605
|
+
"include_files",
|
|
606
|
+
Status.WARN,
|
|
607
|
+
"drift vs. the lock: " + ", ".join(drifted),
|
|
608
|
+
hint="run `kivyforge lock -p android`; the build refuses to stage drift.",
|
|
609
|
+
)
|
|
610
|
+
return CheckResult(
|
|
611
|
+
"include_files", Status.PASS, f"{len(staged)} file(s) match the lock"
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _check_find_links(config: Config, project_root: Path) -> CheckResult:
|
|
616
|
+
"""A `find_links` dir that has moved or holds no wheels only shows up as an
|
|
617
|
+
unresolvable requirement at lock time; name it here instead."""
|
|
618
|
+
from kivyforge.lock.find_links import find_links_doctor_detail
|
|
619
|
+
|
|
620
|
+
entries = config.android_required.find_links
|
|
621
|
+
if not entries:
|
|
622
|
+
return CheckResult("find_links directories", Status.SKIP, "not configured")
|
|
623
|
+
root = project_root.resolve()
|
|
624
|
+
problems: list[tuple[str, str | None]] = []
|
|
625
|
+
empty: list[tuple[str, str | None]] = []
|
|
626
|
+
ok: list[str] = []
|
|
627
|
+
for entry in entries:
|
|
628
|
+
path = (root / entry).resolve()
|
|
629
|
+
detail, hint = find_links_doctor_detail(root, entry, path, platform="android")
|
|
630
|
+
if not path.is_dir():
|
|
631
|
+
problems.append((detail, hint))
|
|
632
|
+
elif not any(path.glob("*.whl")):
|
|
633
|
+
empty.append((detail, hint))
|
|
634
|
+
else:
|
|
635
|
+
ok.append(detail)
|
|
636
|
+
for bucket, status in ((problems, Status.FAIL), (empty, Status.WARN)):
|
|
637
|
+
if bucket:
|
|
638
|
+
return CheckResult(
|
|
639
|
+
"find_links directories",
|
|
640
|
+
status,
|
|
641
|
+
"; ".join(d for d, _ in bucket),
|
|
642
|
+
hint=next((h for _, h in bucket if h), ""),
|
|
643
|
+
)
|
|
644
|
+
return CheckResult("find_links directories", Status.PASS, "; ".join(ok))
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _check_app_native_binaries(config: Config, project_root: Path) -> CheckResult:
|
|
648
|
+
"""Native code in ``app_dir`` cannot work on Android, whatever its ABI.
|
|
649
|
+
|
|
650
|
+
``app_dir`` is staged into the Python *asset* bundle and unpacked to
|
|
651
|
+
app-private storage at first launch, and Android refuses to ``dlopen`` a
|
|
652
|
+
library from there (W^X). Only ``jniLibs/`` — which kivyforge fills from
|
|
653
|
+
wheels — is a loadable location, so a ``.so`` here is dead weight at best.
|
|
654
|
+
"""
|
|
655
|
+
from .elf import ElfError, machine_name, read_elf
|
|
656
|
+
|
|
657
|
+
app_dir = project_root / config.kivy.app_dir
|
|
658
|
+
if not app_dir.is_dir():
|
|
659
|
+
return CheckResult(
|
|
660
|
+
"App-local native binaries",
|
|
661
|
+
Status.SKIP,
|
|
662
|
+
f"app_dir does not exist: {app_dir}",
|
|
663
|
+
)
|
|
664
|
+
found: list[str] = []
|
|
665
|
+
for pattern in ("*.so", "*.dylib", "*.dll", "*.pyd"):
|
|
666
|
+
for binary in sorted(app_dir.rglob(pattern)):
|
|
667
|
+
rel = binary.relative_to(project_root).as_posix()
|
|
668
|
+
try:
|
|
669
|
+
info = read_elf(binary)
|
|
670
|
+
except (ElfError, OSError):
|
|
671
|
+
found.append(f"{rel} (not an ELF)")
|
|
672
|
+
else:
|
|
673
|
+
found.append(f"{rel} ({machine_name(info.machine)})")
|
|
674
|
+
if found:
|
|
675
|
+
return CheckResult(
|
|
676
|
+
"App-local native binaries",
|
|
677
|
+
Status.FAIL,
|
|
678
|
+
"; ".join(found),
|
|
679
|
+
hint="native code belongs in an Android wheel (jniLibs), not "
|
|
680
|
+
"app_dir: the asset bundle is unpacked to app-private storage, "
|
|
681
|
+
"which Android will not dlopen (android/03 §wheel content rules).",
|
|
682
|
+
)
|
|
683
|
+
return CheckResult(
|
|
684
|
+
"App-local native binaries", Status.PASS, "no native binaries in app_dir"
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _check_manifest_policy(config: Config) -> CheckResult:
|
|
689
|
+
"""Run `package`'s own release policy over the manifest kivyforge would
|
|
690
|
+
generate, so a footgun surfaces now rather than at release time.
|
|
691
|
+
|
|
692
|
+
This is the preflight's first pass only: the merged-manifest pass needs AGP,
|
|
693
|
+
so `kivyforge package` remains the gate that sees library manifests.
|
|
694
|
+
"""
|
|
695
|
+
from .generate.manifest import generate_manifest
|
|
696
|
+
from .policy import check_release_manifest
|
|
697
|
+
|
|
698
|
+
android = config.android_required
|
|
699
|
+
try:
|
|
700
|
+
manifest_xml = generate_manifest(android, orientation=config.kivy.orientation)
|
|
701
|
+
except Exception as exc: # noqa: BLE001 - a generation failure is its own report
|
|
702
|
+
return CheckResult(
|
|
703
|
+
"Manifest policy (release)",
|
|
704
|
+
Status.FAIL,
|
|
705
|
+
f"the manifest could not be generated: {exc}",
|
|
706
|
+
)
|
|
707
|
+
findings = check_release_manifest(
|
|
708
|
+
manifest_xml,
|
|
709
|
+
package=android.package,
|
|
710
|
+
allow_exported=android.manifest.allow_exported,
|
|
711
|
+
)
|
|
712
|
+
fails = [f.message for f in findings if f.severity == "FAIL"]
|
|
713
|
+
if fails:
|
|
714
|
+
return CheckResult(
|
|
715
|
+
"Manifest policy (release)",
|
|
716
|
+
Status.FAIL,
|
|
717
|
+
"; ".join(fails),
|
|
718
|
+
hint="`kivyforge package` blocks on these before signing; fix them "
|
|
719
|
+
"through the manifest escape hatches (android/04 §release policy).",
|
|
720
|
+
)
|
|
721
|
+
infos = [f.message for f in findings if f.severity == "INFO"]
|
|
722
|
+
if infos:
|
|
723
|
+
return CheckResult(
|
|
724
|
+
"Manifest policy (release)",
|
|
725
|
+
Status.PASS,
|
|
726
|
+
"no violations; advisory: " + "; ".join(infos),
|
|
727
|
+
)
|
|
728
|
+
return CheckResult("Manifest policy (release)", Status.PASS, "no violations")
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _check_implied_features(config: Config) -> CheckResult:
|
|
732
|
+
"""Report the `<uses-feature>` set `auto_features` will synthesize.
|
|
733
|
+
|
|
734
|
+
Play filters devices on these, so a permission quietly narrowing the store
|
|
735
|
+
audience is worth seeing before upload rather than after.
|
|
736
|
+
"""
|
|
737
|
+
from .generate.manifest import implied_features, qualified_permissions
|
|
738
|
+
|
|
739
|
+
android = config.android_required
|
|
740
|
+
if not android.permissions.auto_features:
|
|
741
|
+
return CheckResult(
|
|
742
|
+
"Implied features", Status.SKIP, "permissions.auto_features is false"
|
|
743
|
+
)
|
|
744
|
+
explicit = {f.name for f in android.permissions.features}
|
|
745
|
+
permissions = qualified_permissions(android.permissions.uses)
|
|
746
|
+
synthesized = [f for f in implied_features(permissions) if f not in explicit]
|
|
747
|
+
if not synthesized:
|
|
748
|
+
return CheckResult(
|
|
749
|
+
"Implied features", Status.PASS, "no permission implies a feature"
|
|
750
|
+
)
|
|
751
|
+
return CheckResult(
|
|
752
|
+
"Implied features",
|
|
753
|
+
Status.PASS,
|
|
754
|
+
'synthesized android:required="false": ' + ", ".join(synthesized),
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _check_lock_hosts(probe: AndroidProbe, lock) -> CheckResult:
|
|
759
|
+
from urllib.parse import urlparse
|
|
760
|
+
|
|
761
|
+
hosts: set[str] = set()
|
|
762
|
+
for runtime in lock.python_android:
|
|
763
|
+
if runtime.url:
|
|
764
|
+
hosts.add(urlparse(runtime.url).hostname or "")
|
|
765
|
+
for package in lock.packages:
|
|
766
|
+
for wheel in package.wheels:
|
|
767
|
+
if wheel.url:
|
|
768
|
+
hosts.add(urlparse(wheel.url).hostname or "")
|
|
769
|
+
# Channels 3 and 4 fetch too: a hosted .aar/.jar, and — when Maven
|
|
770
|
+
# coordinates are declared — Gradle's own repositories (the defaults it
|
|
771
|
+
# always consults plus any extra the project declares).
|
|
772
|
+
for lib in lock.android_libs:
|
|
773
|
+
if lib.url:
|
|
774
|
+
hosts.add(urlparse(lib.url).hostname or "")
|
|
775
|
+
if lock.gradle.declared:
|
|
776
|
+
hosts |= {"dl.google.com", "repo.maven.apache.org"}
|
|
777
|
+
for repo in lock.gradle.repositories:
|
|
778
|
+
hosts.add(urlparse(repo).hostname or "")
|
|
779
|
+
hosts.discard("")
|
|
780
|
+
if not hosts:
|
|
781
|
+
return CheckResult(
|
|
782
|
+
"Required hosts reachable", Status.SKIP, "all artifacts are vendored"
|
|
783
|
+
)
|
|
784
|
+
unreachable = [h for h in sorted(hosts) if not probe.tcp_reachable(h, 443)]
|
|
785
|
+
if unreachable:
|
|
786
|
+
return CheckResult(
|
|
787
|
+
"Required hosts reachable",
|
|
788
|
+
Status.WARN,
|
|
789
|
+
f"cannot reach: {', '.join(unreachable)}",
|
|
790
|
+
hint="check your network; needed to fetch runtime/wheels.",
|
|
791
|
+
)
|
|
792
|
+
return CheckResult(
|
|
793
|
+
"Required hosts reachable", Status.PASS, f"{len(hosts)} host(s) reachable"
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def android_doctor(
|
|
798
|
+
cwd: Path,
|
|
799
|
+
*,
|
|
800
|
+
kivyforge_version: str,
|
|
801
|
+
offline: bool,
|
|
802
|
+
probe: AndroidProbe | None = None,
|
|
803
|
+
) -> list[CheckResult]:
|
|
804
|
+
probe = probe or RealAndroidProbe()
|
|
805
|
+
sdk = probe.sdk_root()
|
|
806
|
+
|
|
807
|
+
config: Config | None = None
|
|
808
|
+
pyproject = cwd / "pyproject.toml"
|
|
809
|
+
config_result: CheckResult | None = None
|
|
810
|
+
if pyproject.exists():
|
|
811
|
+
try:
|
|
812
|
+
config = load_config(pyproject, require_ios=False, require_android=True)
|
|
813
|
+
except ConfigError as exc:
|
|
814
|
+
config_result = CheckResult(
|
|
815
|
+
"Android config",
|
|
816
|
+
Status.FAIL,
|
|
817
|
+
str(exc).splitlines()[0],
|
|
818
|
+
hint="fix pyproject.toml; see the error above.",
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
android = config.android if config else None
|
|
822
|
+
results = [
|
|
823
|
+
_check_kivyforge_version(probe, kivyforge_version, offline=offline),
|
|
824
|
+
_check_jdk(probe),
|
|
825
|
+
_check_sdk(sdk),
|
|
826
|
+
_check_sdk_licenses(probe, sdk),
|
|
827
|
+
_check_build_tools(probe, sdk, android),
|
|
828
|
+
_check_ndk(probe, sdk),
|
|
829
|
+
_check_adb(probe, sdk),
|
|
830
|
+
_check_emulator(probe, sdk),
|
|
831
|
+
]
|
|
832
|
+
|
|
833
|
+
if config_result is not None:
|
|
834
|
+
results.append(config_result)
|
|
835
|
+
return results
|
|
836
|
+
if config is None:
|
|
837
|
+
return results # environment mode
|
|
838
|
+
|
|
839
|
+
android = config.android_required
|
|
840
|
+
results.append(
|
|
841
|
+
CheckResult(
|
|
842
|
+
"Android config",
|
|
843
|
+
Status.PASS,
|
|
844
|
+
f"{android.package} (min {android.min_sdk} / target "
|
|
845
|
+
f"{android.target_sdk}, kivy_generation {android.kivy_generation}, "
|
|
846
|
+
f"abis {', '.join(android.abis)})",
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
app_dir = cwd / config.kivy.app_dir
|
|
850
|
+
results.append(
|
|
851
|
+
CheckResult("App source directory", Status.PASS, str(app_dir))
|
|
852
|
+
if app_dir.is_dir()
|
|
853
|
+
else CheckResult(
|
|
854
|
+
"App source directory",
|
|
855
|
+
Status.FAIL,
|
|
856
|
+
f"[tool.kivy].app_dir does not exist: {app_dir}",
|
|
857
|
+
hint="create it or fix app_dir.",
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
results.append(_check_app_native_binaries(config, cwd))
|
|
861
|
+
results.append(_check_icon(config, cwd))
|
|
862
|
+
results.append(_check_splash(config, cwd))
|
|
863
|
+
results.append(_check_find_links(config, cwd))
|
|
864
|
+
results.append(_check_manifest_policy(config))
|
|
865
|
+
results.append(_check_implied_features(config))
|
|
866
|
+
|
|
867
|
+
lock_path = cwd / "pylock.android.toml"
|
|
868
|
+
if lock_path.is_file():
|
|
869
|
+
try:
|
|
870
|
+
lock = lock_reader.load(lock_path)
|
|
871
|
+
except Exception as exc: # noqa: BLE001
|
|
872
|
+
results.append(CheckResult("Lock", Status.FAIL, f"unreadable lock: {exc}"))
|
|
873
|
+
return results
|
|
874
|
+
results += [
|
|
875
|
+
_check_sdl_kivy_match(config, lock),
|
|
876
|
+
_check_pyjnius_contract(lock),
|
|
877
|
+
_check_abi_coverage(config, lock),
|
|
878
|
+
_check_include_files(config, cwd, lock),
|
|
879
|
+
]
|
|
880
|
+
if not offline:
|
|
881
|
+
results.append(_check_lock_hosts(probe, lock))
|
|
882
|
+
else:
|
|
883
|
+
results.append(
|
|
884
|
+
CheckResult(
|
|
885
|
+
"Lock",
|
|
886
|
+
Status.WARN,
|
|
887
|
+
"pylock.android.toml not found",
|
|
888
|
+
hint="run `kivyforge lock -p android`.",
|
|
889
|
+
)
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
from .cli import project_dir_for
|
|
893
|
+
|
|
894
|
+
project_dir = project_dir_for(cwd, config)
|
|
895
|
+
results.append(_check_gradle_wrapper(project_dir))
|
|
896
|
+
if project_dir.is_dir():
|
|
897
|
+
results.append(_check_16k_alignment(project_dir))
|
|
898
|
+
results.append(_check_signing(config, cwd))
|
|
899
|
+
return results
|