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,462 @@
|
|
|
1
|
+
"""Generate the Gradle/AGP project files (android/04).
|
|
2
|
+
|
|
3
|
+
Everything here is regenerated on every build (``<app>-android/`` is a
|
|
4
|
+
managed artifact); emission is deterministic — sorted dependency and property
|
|
5
|
+
lines, stable attribute order — so re-runs are byte-identical.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import stat
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from kivyforge.config.model import AndroidConfig, Config
|
|
15
|
+
|
|
16
|
+
from .. import policy, toolchain
|
|
17
|
+
from ..lock.model import AndroidLockfile
|
|
18
|
+
|
|
19
|
+
# Wheel-tag ABI spelling -> Android's dashed directory/filter names.
|
|
20
|
+
ABI_TO_ANDROID = {"arm64_v8a": "arm64-v8a", "x86_64": "x86_64"}
|
|
21
|
+
|
|
22
|
+
# Wheel-tag ABI spelling -> the NDK target triplet (CMAKE_LIBRARY_ARCHITECTURE);
|
|
23
|
+
# the runtime staging area is keyed by triplet so CMake's {{triplet}}
|
|
24
|
+
# substitution (see the CMakeLists template) finds each ABI's own prefix.
|
|
25
|
+
ABI_TO_TRIPLET = {
|
|
26
|
+
"arm64_v8a": "aarch64-linux-android",
|
|
27
|
+
"x86_64": "x86_64-linux-android",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_WRAPPER_DIR = Path(__file__).parent.parent / "gradle_wrapper"
|
|
31
|
+
|
|
32
|
+
# The generated task that copies AGP's merged release manifest somewhere stable,
|
|
33
|
+
# and where it lands (relative to the generated project root). `kivyforge
|
|
34
|
+
# package` runs the task, then lints the copy.
|
|
35
|
+
MERGED_MANIFEST_TASK = "exportKivyforgeReleaseManifest"
|
|
36
|
+
MERGED_MANIFEST_RELPATH = Path(
|
|
37
|
+
"app", "build", "kivyforge", "AndroidManifest-merged-release.xml"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProjectGenError(Exception):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def android_abi(abi: str) -> str:
|
|
46
|
+
return ABI_TO_ANDROID[abi]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def write_settings_gradle(dest: Path, android: AndroidConfig | None = None) -> None:
|
|
50
|
+
"""Emit ``settings.gradle``, including any declared extra Maven repos.
|
|
51
|
+
|
|
52
|
+
``[tool.kivy.android.gradle].repositories`` has to be honored here as well
|
|
53
|
+
as at lock time: the lock resolves the graph in a scratch project, so a
|
|
54
|
+
coordinate that only exists in a private repo would lock fine and then fail
|
|
55
|
+
to resolve in the generated build (android/01 §gradle).
|
|
56
|
+
"""
|
|
57
|
+
extra = tuple(android.gradle.repositories) if android is not None else ()
|
|
58
|
+
repos = [" google()", " mavenCentral()"]
|
|
59
|
+
repos += [f" maven {{ url = uri({_groovy_str(url)}) }}" for url in extra]
|
|
60
|
+
_write(
|
|
61
|
+
dest / "settings.gradle",
|
|
62
|
+
"pluginManagement {\n"
|
|
63
|
+
" repositories { google(); mavenCentral(); gradlePluginPortal() }\n"
|
|
64
|
+
"}\n"
|
|
65
|
+
"dependencyResolutionManagement {\n"
|
|
66
|
+
" repositories {\n" + "\n".join(repos) + "\n }\n"
|
|
67
|
+
"}\n"
|
|
68
|
+
'rootProject.name = "kivyforge-app"\n'
|
|
69
|
+
'include ":app"\n',
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _groovy_str(value: str) -> str:
|
|
74
|
+
"""A single-quoted Groovy string literal."""
|
|
75
|
+
return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def write_root_build_gradle(dest: Path, android: AndroidConfig) -> None:
|
|
79
|
+
lines = [
|
|
80
|
+
"plugins {",
|
|
81
|
+
f" id 'com.android.application' version '{toolchain.AGP_VERSION}' "
|
|
82
|
+
"apply false",
|
|
83
|
+
]
|
|
84
|
+
if android.src.kotlin:
|
|
85
|
+
lines.append(
|
|
86
|
+
f" id 'org.jetbrains.kotlin.android' version "
|
|
87
|
+
f"'{toolchain.KOTLIN_VERSION}' apply false"
|
|
88
|
+
)
|
|
89
|
+
lines.append("}")
|
|
90
|
+
_write(dest / "build.gradle", "\n".join(lines) + "\n")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def write_gradle_properties(dest: Path, android: AndroidConfig) -> None:
|
|
94
|
+
managed = {
|
|
95
|
+
"android.useAndroidX": "true",
|
|
96
|
+
"org.gradle.jvmargs": "-Xmx2048m -Dfile.encoding=UTF-8",
|
|
97
|
+
}
|
|
98
|
+
merged = dict(managed)
|
|
99
|
+
for key, value in android.gradle_properties.items():
|
|
100
|
+
# Reserved keys were rejected at config time (rule 15).
|
|
101
|
+
merged[str(key)] = _prop_str(value)
|
|
102
|
+
lines = [f"{k}={v}" for k, v in sorted(merged.items())]
|
|
103
|
+
_write(dest / "gradle.properties", "\n".join(lines) + "\n")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def stage_gradle_wrapper(dest: Path) -> None:
|
|
107
|
+
"""Stage the vendored wrapper + the pinned distribution properties."""
|
|
108
|
+
wrapper_dir = dest / "gradle" / "wrapper"
|
|
109
|
+
wrapper_dir.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
jar = _WRAPPER_DIR / "gradle-wrapper.jar"
|
|
111
|
+
if not jar.is_file():
|
|
112
|
+
raise ProjectGenError(
|
|
113
|
+
f"vendored gradle-wrapper.jar missing at {jar}; the kivyforge "
|
|
114
|
+
"installation is incomplete."
|
|
115
|
+
)
|
|
116
|
+
shutil.copy2(jar, wrapper_dir / "gradle-wrapper.jar")
|
|
117
|
+
_write(
|
|
118
|
+
wrapper_dir / "gradle-wrapper.properties",
|
|
119
|
+
"distributionBase=GRADLE_USER_HOME\n"
|
|
120
|
+
"distributionPath=wrapper/dists\n"
|
|
121
|
+
f"distributionUrl={toolchain.GRADLE_DISTRIBUTION_URL}\n"
|
|
122
|
+
"zipStoreBase=GRADLE_USER_HOME\n"
|
|
123
|
+
"zipStorePath=wrapper/dists\n",
|
|
124
|
+
)
|
|
125
|
+
for script in ("gradlew", "gradlew.bat"):
|
|
126
|
+
source = _WRAPPER_DIR / script
|
|
127
|
+
target = dest / script
|
|
128
|
+
shutil.copy2(source, target)
|
|
129
|
+
if script == "gradlew":
|
|
130
|
+
target.chmod(target.stat().st_mode | stat.S_IEXEC)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def write_app_build_gradle(
|
|
134
|
+
dest: Path,
|
|
135
|
+
config: Config,
|
|
136
|
+
android: AndroidConfig,
|
|
137
|
+
*,
|
|
138
|
+
python_version: str,
|
|
139
|
+
runtime_root: Path,
|
|
140
|
+
staged_libs: list[str],
|
|
141
|
+
abis: tuple[str, ...] | None = None,
|
|
142
|
+
signing_config_block: str = "",
|
|
143
|
+
release_signing_config: str | None = None,
|
|
144
|
+
test_build_type: str | None = None,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Emit ``app/build.gradle``.
|
|
147
|
+
|
|
148
|
+
``abis`` is the *effective* ABI set for this build (a ``--abi`` restriction
|
|
149
|
+
must narrow the Gradle abiFilters too, or AGP builds ABIs whose runtime was
|
|
150
|
+
never staged); defaults to the full configured set.
|
|
151
|
+
|
|
152
|
+
``release_signing_config`` names the signing config the release build type
|
|
153
|
+
uses (default: ``release`` when a ``signing_config_block`` was supplied,
|
|
154
|
+
otherwise unsigned). ``test_build_type`` selects which variant the
|
|
155
|
+
instrumented tests run against — AGP only generates
|
|
156
|
+
``connected<Variant>AndroidTest`` for that one variant, so the release smoke
|
|
157
|
+
test needs it set to ``release``.
|
|
158
|
+
|
|
159
|
+
``runtime_root`` is the staging directory holding one extracted runtime
|
|
160
|
+
per target triplet (``<runtime_root>/<triplet>/prefix``). AGP invokes
|
|
161
|
+
CMake once per ABI; the ``{{triplet}}`` placeholder in ``PYTHON_PREFIX_DIR``
|
|
162
|
+
is resolved *inside* CMake via ``CMAKE_LIBRARY_ARCHITECTURE`` (the
|
|
163
|
+
python.org testbed's mechanism), so each ABI's launcher links its own
|
|
164
|
+
libpython — a Gradle-side lookup cannot do this for plain ``gradlew``
|
|
165
|
+
builds (``android.injected.build.abi`` is Studio-only).
|
|
166
|
+
"""
|
|
167
|
+
effective = abis if abis is not None else tuple(android.abis)
|
|
168
|
+
abis_filter = ", ".join(f"'{android_abi(a)}'" for a in effective)
|
|
169
|
+
stem = "python" + ".".join(python_version.split(".")[:2])
|
|
170
|
+
prefix_arg = _gradle_path(runtime_root) + "/{{triplet}}/prefix"
|
|
171
|
+
|
|
172
|
+
deps: list[str] = [
|
|
173
|
+
" implementation 'com.google.android.material:material:1.11.0'",
|
|
174
|
+
# The generated contract smoke test (android/06 --smoke).
|
|
175
|
+
" androidTestImplementation 'androidx.test.ext:junit:1.1.5'",
|
|
176
|
+
" androidTestImplementation 'androidx.test:core:1.5.0'",
|
|
177
|
+
" androidTestImplementation 'androidx.test:runner:1.5.2'",
|
|
178
|
+
]
|
|
179
|
+
for lib in sorted(staged_libs):
|
|
180
|
+
deps.append(f" implementation files('libs/{lib}')")
|
|
181
|
+
for coordinate in sorted(android.gradle.dependencies):
|
|
182
|
+
deps.append(f" implementation '{coordinate}'")
|
|
183
|
+
|
|
184
|
+
plugins = [" id 'com.android.application'"]
|
|
185
|
+
if android.src.kotlin:
|
|
186
|
+
plugins.append(" id 'org.jetbrains.kotlin.android'")
|
|
187
|
+
|
|
188
|
+
src_dirs = "".join(
|
|
189
|
+
f" java.srcDirs += '{_gradle_path(Path(p))}'\n"
|
|
190
|
+
for p in [*android.src.java, *android.src.kotlin]
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
bs = android.build_settings
|
|
194
|
+
strip = _release_flag(bs.strip_native_libs)
|
|
195
|
+
minify = "true" if bs.minify else "false"
|
|
196
|
+
shrink = "true" if (bs.shrink_resources and bs.minify) else "false"
|
|
197
|
+
# Keep .so debug symbols unless a stripped release is configured (AGP does
|
|
198
|
+
# the strip, tracking the toolchain + preserving 16 KB alignment).
|
|
199
|
+
keep_symbols_line = "" if strip else " keepDebugSymbols += ['**/*.so']\n"
|
|
200
|
+
debug_symbol_level = {
|
|
201
|
+
"symbol_table": "SYMBOL_TABLE",
|
|
202
|
+
"full": "FULL",
|
|
203
|
+
"none": "NONE",
|
|
204
|
+
}[bs.debug_symbols]
|
|
205
|
+
# At minSdk 21+ the platform loads multiple dex files natively, so this is
|
|
206
|
+
# only about whether AGP is *allowed* to split: left on, the bundled .aars
|
|
207
|
+
# plus the bootstrap can exceed the 64K method limit without failing the
|
|
208
|
+
# build; turned off, an over-limit project fails at dexing.
|
|
209
|
+
multidex = "true" if bs.multidex else "false"
|
|
210
|
+
signing_name = release_signing_config or ("release" if signing_config_block else "")
|
|
211
|
+
release_signing = (
|
|
212
|
+
f" signingConfig signingConfigs.{signing_name}\n"
|
|
213
|
+
if signing_name
|
|
214
|
+
else ""
|
|
215
|
+
)
|
|
216
|
+
test_build_type_line = (
|
|
217
|
+
f" testBuildType '{test_build_type}'\n" if test_build_type else ""
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
lint_checks = ", ".join(f"'{check}'" for check in policy.LINT_CHECKS)
|
|
221
|
+
merged_manifest_name = MERGED_MANIFEST_RELPATH.name
|
|
222
|
+
|
|
223
|
+
text = f"""\
|
|
224
|
+
import com.android.build.api.artifact.SingleArtifact
|
|
225
|
+
|
|
226
|
+
plugins {{
|
|
227
|
+
{chr(10).join(plugins)}
|
|
228
|
+
}}
|
|
229
|
+
|
|
230
|
+
android {{
|
|
231
|
+
namespace '{android.package}'
|
|
232
|
+
compileSdk {android.compile_sdk}
|
|
233
|
+
ndkVersion '{toolchain.NDK_VERSION}'
|
|
234
|
+
{test_build_type_line}
|
|
235
|
+
defaultConfig {{
|
|
236
|
+
applicationId '{android.package}'
|
|
237
|
+
minSdk {android.min_sdk}
|
|
238
|
+
targetSdk {android.target_sdk}
|
|
239
|
+
versionCode {android.version_code}
|
|
240
|
+
versionName '{config.project.version}'
|
|
241
|
+
multiDexEnabled {multidex}
|
|
242
|
+
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
|
243
|
+
ndk {{ abiFilters {abis_filter} }}
|
|
244
|
+
externalNativeBuild {{
|
|
245
|
+
cmake {{
|
|
246
|
+
arguments '-DPYTHON_VERSION={stem[6:]}',
|
|
247
|
+
'-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON',
|
|
248
|
+
'-DPYTHON_PREFIX_DIR={prefix_arg}'
|
|
249
|
+
}}
|
|
250
|
+
}}
|
|
251
|
+
}}
|
|
252
|
+
|
|
253
|
+
externalNativeBuild {{
|
|
254
|
+
cmake {{ path 'src/main/cpp/CMakeLists.txt' }}
|
|
255
|
+
}}
|
|
256
|
+
|
|
257
|
+
// AAPT's default ignore pattern drops asset dirs starting with '_', which
|
|
258
|
+
// would silently exclude _python_bundle (loadmodel findings #4).
|
|
259
|
+
aaptOptions.ignoreAssetsPattern = 'kivyforge-dont-ignore-anything'
|
|
260
|
+
|
|
261
|
+
packagingOptions {{
|
|
262
|
+
jniLibs {{
|
|
263
|
+
// Extract native libs so dlopen-by-path works for the flattened
|
|
264
|
+
// extension modules (android/04 §load model).
|
|
265
|
+
useLegacyPackaging = true
|
|
266
|
+
{keep_symbols_line} }}
|
|
267
|
+
}}
|
|
268
|
+
|
|
269
|
+
{signing_config_block}
|
|
270
|
+
buildTypes {{
|
|
271
|
+
release {{
|
|
272
|
+
minifyEnabled {minify}
|
|
273
|
+
shrinkResources {shrink}
|
|
274
|
+
{release_signing} ndk {{ debugSymbolLevel '{debug_symbol_level}' }}
|
|
275
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), \
|
|
276
|
+
'proguard-rules.pro'
|
|
277
|
+
}}
|
|
278
|
+
}}
|
|
279
|
+
|
|
280
|
+
sourceSets {{
|
|
281
|
+
main {{
|
|
282
|
+
{src_dirs} }}
|
|
283
|
+
}}
|
|
284
|
+
|
|
285
|
+
compileOptions {{
|
|
286
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
287
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
288
|
+
}}
|
|
289
|
+
|
|
290
|
+
// The release policy's delegated half (android/06 §package). checkOnly
|
|
291
|
+
// narrows the run to the curated subset and `fatal` raises exactly those to
|
|
292
|
+
// build-breaking, so a future Lint's new checks can never spuriously block a
|
|
293
|
+
// release (and a stale id stays a warning instead of failing every build).
|
|
294
|
+
// `kivyforge package` runs lintRelease before it assembles, so a finding
|
|
295
|
+
// blocks before signing.
|
|
296
|
+
lint {{
|
|
297
|
+
checkOnly.addAll([{lint_checks}])
|
|
298
|
+
fatal.addAll([{lint_checks}])
|
|
299
|
+
abortOnError = true
|
|
300
|
+
checkReleaseBuilds = true
|
|
301
|
+
htmlReport = true
|
|
302
|
+
xmlReport = true
|
|
303
|
+
}}
|
|
304
|
+
}}
|
|
305
|
+
|
|
306
|
+
// kivyforge's own half of the policy lints the *merged* release manifest, so
|
|
307
|
+
// export it to a stable path (AGP's intermediates layout is not a contract).
|
|
308
|
+
androidComponents {{
|
|
309
|
+
onVariants(selector().withName('release')) {{ variant ->
|
|
310
|
+
tasks.register('{MERGED_MANIFEST_TASK}', Copy) {{
|
|
311
|
+
from(variant.artifacts.get(SingleArtifact.MERGED_MANIFEST.INSTANCE))
|
|
312
|
+
into(layout.buildDirectory.dir('kivyforge'))
|
|
313
|
+
rename {{ '{merged_manifest_name}' }}
|
|
314
|
+
}}
|
|
315
|
+
}}
|
|
316
|
+
}}
|
|
317
|
+
|
|
318
|
+
dependencies {{
|
|
319
|
+
{chr(10).join(deps)}
|
|
320
|
+
}}
|
|
321
|
+
"""
|
|
322
|
+
_write(dest / "app" / "build.gradle", text)
|
|
323
|
+
_write(
|
|
324
|
+
dest / "app" / "proguard-rules.pro",
|
|
325
|
+
"# kivyforge keep-rules: pyjnius reflects into these at runtime.\n"
|
|
326
|
+
"-keep class org.jnius.** {{ *; }}\n".replace("{{", "{").replace("}}", "}")
|
|
327
|
+
+ "-keep class org.kivy.android.** { *; }\n"
|
|
328
|
+
"-keep class org.renpy.android.** { *; }\n"
|
|
329
|
+
"-keep class org.libsdl.app.** { *; }\n",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _release_flag(value: bool | str) -> bool:
|
|
334
|
+
"""A build_settings tri-state -> whether it applies to a release build."""
|
|
335
|
+
from kivyforge.config.model import ANDROID_RELEASE_ONLY
|
|
336
|
+
|
|
337
|
+
return value is True or value == ANDROID_RELEASE_ONLY
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def write_resources(
|
|
341
|
+
dest: Path, config: Config, android: AndroidConfig, *, project_root: Path
|
|
342
|
+
) -> None:
|
|
343
|
+
res = dest / "app" / "src" / "main" / "res"
|
|
344
|
+
values = res / "values"
|
|
345
|
+
values.mkdir(parents=True, exist_ok=True)
|
|
346
|
+
_write(
|
|
347
|
+
values / "strings.xml",
|
|
348
|
+
'<resources><string name="app_name">'
|
|
349
|
+
f"{_xml_escape(config.display_name)}</string></resources>\n",
|
|
350
|
+
)
|
|
351
|
+
# The generated manifest names @mipmap/ic_launcher unconditionally, so the
|
|
352
|
+
# icon set is written on every build — the default one when no source is
|
|
353
|
+
# configured, the full adaptive set when there is.
|
|
354
|
+
from ..icons import IconError, write_icons
|
|
355
|
+
from ..splash import SplashError, write_splash
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
write_icons(res, config, android, project_root)
|
|
359
|
+
except IconError as exc:
|
|
360
|
+
raise ProjectGenError(str(exc)) from exc
|
|
361
|
+
try:
|
|
362
|
+
splash = write_splash(res, android, project_root)
|
|
363
|
+
except SplashError as exc:
|
|
364
|
+
raise ProjectGenError(str(exc)) from exc
|
|
365
|
+
_write(values / "styles.xml", _styles_xml(android, splash, v31=False))
|
|
366
|
+
if splash.configured:
|
|
367
|
+
# A values-v31 resource replaces the same-named one wholesale, so the
|
|
368
|
+
# override repeats the whole style rather than adding to it.
|
|
369
|
+
v31 = res / "values-v31"
|
|
370
|
+
v31.mkdir(parents=True, exist_ok=True)
|
|
371
|
+
_write(v31 / "styles.xml", _styles_xml(android, splash, v31=True))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _styles_xml(android: AndroidConfig, splash, *, v31: bool) -> str:
|
|
375
|
+
"""The app theme, with the splash items the given API level understands."""
|
|
376
|
+
items: list[tuple[str, str]] = []
|
|
377
|
+
if splash.window_background is not None:
|
|
378
|
+
items.append(("android:windowBackground", splash.window_background))
|
|
379
|
+
if v31:
|
|
380
|
+
items += list(splash.v31_items)
|
|
381
|
+
if not items:
|
|
382
|
+
return (
|
|
383
|
+
"<resources>\n"
|
|
384
|
+
f' <style name="Theme.Kivyforge" parent="{android.base_theme}" />\n'
|
|
385
|
+
"</resources>\n"
|
|
386
|
+
)
|
|
387
|
+
body = "".join(
|
|
388
|
+
f' <item name="{name}">{value}</item>\n' for name, value in items
|
|
389
|
+
)
|
|
390
|
+
return (
|
|
391
|
+
"<resources>\n"
|
|
392
|
+
f' <style name="Theme.Kivyforge" parent="{android.base_theme}">\n'
|
|
393
|
+
f"{body}"
|
|
394
|
+
" </style>\n"
|
|
395
|
+
"</resources>\n"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def write_gradle_pins(dest: Path, lock: AndroidLockfile) -> None:
|
|
400
|
+
"""Materialize the committed Maven-graph record into the project (android/02).
|
|
401
|
+
|
|
402
|
+
Only when Maven coordinates are declared. The app's declared coordinates are
|
|
403
|
+
version-pinned in ``app/build.gradle`` (``implementation 'g:a:v'``), and the
|
|
404
|
+
**full resolved transitive graph with a per-artifact SHA-256 is the committed
|
|
405
|
+
audit record in ``pylock.android.toml``** (``[[tool.kivyforge.gradle.resolved]]``);
|
|
406
|
+
`write_gradle_pins` mirrors that record into ``app/gradle.lockfile`` as
|
|
407
|
+
comments, for reference. Neither Gradle dependency locking nor Gradle artifact
|
|
408
|
+
verification is enabled, so this channel's hashes are audited, not enforced —
|
|
409
|
+
the docs say so in those words (android/02 §Gradle pins).
|
|
410
|
+
|
|
411
|
+
> **Scoped in v1.** Gradle's ``verification-metadata.xml`` enforces SHA-256
|
|
412
|
+
> over the *entire* build classpath (AGP, androidx, transforms), not just the
|
|
413
|
+
> app's Maven deps — which the lock does not resolve. Emitting a partial
|
|
414
|
+
> metadata file makes Gradle reject every unlisted build artifact, so v1
|
|
415
|
+
> records the app-dep hashes in the lock (the auditable source of truth) and
|
|
416
|
+
> defers Gradle-*enforced* verification to a future full-classpath resolve.
|
|
417
|
+
> See docs/design/platforms/android/02 §Gradle pins (realized-vs-designed).
|
|
418
|
+
"""
|
|
419
|
+
# Remove any stale strict metadata a prior kivyforge emitted (it would make
|
|
420
|
+
# Gradle verify the entire build classpath and fail) — the generated project
|
|
421
|
+
# is regenerated every build, so leftovers must be swept.
|
|
422
|
+
stale = dest / "gradle" / "verification-metadata.xml"
|
|
423
|
+
if stale.exists():
|
|
424
|
+
stale.unlink()
|
|
425
|
+
if not lock.gradle.declared:
|
|
426
|
+
return
|
|
427
|
+
lock_lines = [
|
|
428
|
+
"# Generated by kivyforge from pylock.android.toml — the resolved Maven",
|
|
429
|
+
"# graph (per-artifact SHA-256) is committed in pylock.android.toml.",
|
|
430
|
+
"#",
|
|
431
|
+
"# INFORMATIONAL. Gradle dependency locking is not enabled in this project,",
|
|
432
|
+
"# so this file gates nothing: it mirrors the lock's audit record at the",
|
|
433
|
+
"# path a Gradle lock would live. Declared coordinates are version-pinned",
|
|
434
|
+
"# in app/build.gradle. See docs/design/platforms/android/02 §Gradle pins.",
|
|
435
|
+
]
|
|
436
|
+
for module in sorted(lock.gradle.resolved, key=lambda m: m.coordinate):
|
|
437
|
+
for artifact in module.artifacts:
|
|
438
|
+
lock_lines.append(
|
|
439
|
+
f"# {module.coordinate} {artifact.name} sha256:{artifact.sha256}"
|
|
440
|
+
)
|
|
441
|
+
_write(dest / "app" / "gradle.lockfile", "\n".join(lock_lines) + "\n")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _write(path: Path, text: str) -> None:
|
|
445
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
446
|
+
path.write_text(text, encoding="utf-8", newline="\n")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _gradle_path(path: Path) -> str:
|
|
450
|
+
return str(path).replace("\\", "/")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _prop_str(value: object) -> str:
|
|
454
|
+
if isinstance(value, bool):
|
|
455
|
+
return "true" if value else "false"
|
|
456
|
+
return str(value)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _xml_escape(text: str) -> str:
|
|
460
|
+
from xml.sax.saxutils import escape
|
|
461
|
+
|
|
462
|
+
return escape(text)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Generate the ``PythonService`` subclasses for ``[[services]]`` (android/05).
|
|
2
|
+
|
|
3
|
+
The manifest declares ``org.kivy.android.Service<Name>`` for every configured
|
|
4
|
+
service; a manifest entry with no class behind it compiles and links fine and
|
|
5
|
+
then throws ``ClassNotFoundException`` the first time anything starts the
|
|
6
|
+
service. These are the classes behind those names: each one is a few overrides
|
|
7
|
+
naming its Python entry point and, for a foreground service, the notification
|
|
8
|
+
the OS requires.
|
|
9
|
+
|
|
10
|
+
The instrumented service contract test is generated alongside them (and only
|
|
11
|
+
when a service exists — there is nothing to test otherwise). It starts the first
|
|
12
|
+
declared service and waits for the ready marker the interpreter stamps, which is
|
|
13
|
+
what proves the whole chain: the manifest name resolves, the service process
|
|
14
|
+
starts, the bundle unpacks in a second process, and the interpreter comes up.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from kivyforge.config.model import AndroidService
|
|
20
|
+
|
|
21
|
+
from ..bootstrap.render import RenderedFile
|
|
22
|
+
|
|
23
|
+
SERVICE_PACKAGE = "org.kivy.android"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def service_class_simple_name(service: AndroidService) -> str:
|
|
27
|
+
"""``Downloader`` -> ``ServiceDownloader`` (matches the manifest emitter)."""
|
|
28
|
+
return f"Service{service.name}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_service_classes(services: tuple[AndroidService, ...]) -> list[RenderedFile]:
|
|
32
|
+
"""One ``PythonService`` subclass per declared service."""
|
|
33
|
+
out: list[RenderedFile] = []
|
|
34
|
+
for service in services:
|
|
35
|
+
name = service_class_simple_name(service)
|
|
36
|
+
out.append(
|
|
37
|
+
RenderedFile(
|
|
38
|
+
f"java/org/kivy/android/{name}.java",
|
|
39
|
+
_service_source(service, class_name=name),
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _service_source(service: AndroidService, *, class_name: str) -> str:
|
|
46
|
+
overrides = [
|
|
47
|
+
_override("String", "getServiceName", _java_string(service.name)),
|
|
48
|
+
_override("String", "getEntryPoint", _java_string(service.entry_point)),
|
|
49
|
+
]
|
|
50
|
+
if service.foreground:
|
|
51
|
+
overrides.append(_override("boolean", "isForeground", "true"))
|
|
52
|
+
note = service.notification
|
|
53
|
+
if note is not None:
|
|
54
|
+
overrides += [
|
|
55
|
+
_override("String", "getChannelId", _java_string(note.channel_id)),
|
|
56
|
+
_override("String", "getChannelName", _java_string(note.channel_name)),
|
|
57
|
+
_override("String", "getNotificationTitle", _java_string(note.title)),
|
|
58
|
+
_override("String", "getNotificationText", _java_string(note.text)),
|
|
59
|
+
_override(
|
|
60
|
+
"String",
|
|
61
|
+
"getNotificationIcon",
|
|
62
|
+
_java_string(note.icon) if note.icon else "null",
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
return (
|
|
66
|
+
f"package {SERVICE_PACKAGE};\n"
|
|
67
|
+
"\n"
|
|
68
|
+
"/**\n"
|
|
69
|
+
f" * Generated from [[tool.kivy.android.services]] entry {service.name!r}\n"
|
|
70
|
+
" * (android/05 §services). Regenerated on every build — edit\n"
|
|
71
|
+
" * pyproject.toml, not this file.\n"
|
|
72
|
+
" */\n"
|
|
73
|
+
f"public class {class_name} extends PythonService {{\n"
|
|
74
|
+
+ "\n".join(overrides)
|
|
75
|
+
+ "}\n"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _override(return_type: str, method: str, value: str) -> str:
|
|
80
|
+
return (
|
|
81
|
+
" @Override\n"
|
|
82
|
+
f" protected {return_type} {method}() {{\n"
|
|
83
|
+
f" return {value};\n"
|
|
84
|
+
" }\n"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _java_string(value: str) -> str:
|
|
89
|
+
escaped = (
|
|
90
|
+
value.replace("\\", "\\\\")
|
|
91
|
+
.replace('"', '\\"')
|
|
92
|
+
.replace("\n", "\\n")
|
|
93
|
+
.replace("\r", "\\r")
|
|
94
|
+
)
|
|
95
|
+
return f'"{escaped}"'
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def render_service_contract_test(
|
|
99
|
+
services: tuple[AndroidService, ...],
|
|
100
|
+
) -> list[RenderedFile]:
|
|
101
|
+
"""The instrumented service probe, or nothing when no service is declared."""
|
|
102
|
+
if not services:
|
|
103
|
+
return []
|
|
104
|
+
service = services[0]
|
|
105
|
+
class_name = service_class_simple_name(service)
|
|
106
|
+
source = f"""\
|
|
107
|
+
package org.kivyforge.test;
|
|
108
|
+
|
|
109
|
+
import static org.junit.Assert.assertFalse;
|
|
110
|
+
import static org.junit.Assert.assertTrue;
|
|
111
|
+
|
|
112
|
+
import android.content.Context;
|
|
113
|
+
import android.content.Intent;
|
|
114
|
+
import androidx.test.core.app.ApplicationProvider;
|
|
115
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
116
|
+
import java.io.File;
|
|
117
|
+
import java.nio.charset.StandardCharsets;
|
|
118
|
+
import java.nio.file.Files;
|
|
119
|
+
import org.junit.Test;
|
|
120
|
+
import org.junit.runner.RunWith;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The kivyforge service contract test, generated for [[services]] entry
|
|
124
|
+
* {service.name!r} (android/05 §services, android/06 --smoke).
|
|
125
|
+
*
|
|
126
|
+
* A declared service is only real if its manifest name resolves to a class, its
|
|
127
|
+
* process starts, and an interpreter comes up there. The service's Python
|
|
128
|
+
* normally never returns, so the probe waits for the marker the launcher stamps
|
|
129
|
+
* once the interpreter is up and the finder installed.
|
|
130
|
+
*/
|
|
131
|
+
@RunWith(AndroidJUnit4.class)
|
|
132
|
+
public class KivyforgeServiceContractTest {{
|
|
133
|
+
|
|
134
|
+
private static final int TIMEOUT_MS = 90_000;
|
|
135
|
+
private static final String MARKER = "kivyforge_service_{service.name}.txt";
|
|
136
|
+
|
|
137
|
+
@Test
|
|
138
|
+
public void declaredServiceStartsAnInterpreter() throws Exception {{
|
|
139
|
+
Context ctx = ApplicationProvider.getApplicationContext();
|
|
140
|
+
File marker = new File(ctx.getFilesDir(), MARKER);
|
|
141
|
+
marker.delete();
|
|
142
|
+
|
|
143
|
+
Intent intent = new Intent(ctx, org.kivy.android.{class_name}.class);
|
|
144
|
+
ctx.startService(intent);
|
|
145
|
+
|
|
146
|
+
long deadline = System.currentTimeMillis() + TIMEOUT_MS;
|
|
147
|
+
String contents = "";
|
|
148
|
+
while (System.currentTimeMillis() < deadline) {{
|
|
149
|
+
if (marker.exists()) {{
|
|
150
|
+
contents = new String(
|
|
151
|
+
Files.readAllBytes(marker.toPath()), StandardCharsets.UTF_8);
|
|
152
|
+
if (contents.contains("SERVICE_READY")) {{
|
|
153
|
+
break;
|
|
154
|
+
}}
|
|
155
|
+
}}
|
|
156
|
+
Thread.sleep(500);
|
|
157
|
+
}}
|
|
158
|
+
assertTrue(
|
|
159
|
+
"service {service.name} did not start an interpreter within "
|
|
160
|
+
+ TIMEOUT_MS + "ms; got: " + contents,
|
|
161
|
+
contents.contains("SERVICE_READY"));
|
|
162
|
+
|
|
163
|
+
// The entry-point import happens right after the marker, so give it a
|
|
164
|
+
// moment and confirm it did not blow up.
|
|
165
|
+
Thread.sleep(2000);
|
|
166
|
+
if (marker.exists()) {{
|
|
167
|
+
contents = new String(
|
|
168
|
+
Files.readAllBytes(marker.toPath()), StandardCharsets.UTF_8);
|
|
169
|
+
}}
|
|
170
|
+
assertFalse(
|
|
171
|
+
"service entry point {service.entry_point} raised on import: "
|
|
172
|
+
+ contents,
|
|
173
|
+
contents.contains("SERVICE_ENTRY_FAILED"));
|
|
174
|
+
assertFalse(
|
|
175
|
+
"service native start failed: " + contents,
|
|
176
|
+
contents.contains("SERVICE_START_FAILED"));
|
|
177
|
+
|
|
178
|
+
ctx.stopService(intent);
|
|
179
|
+
}}
|
|
180
|
+
}}
|
|
181
|
+
"""
|
|
182
|
+
return [
|
|
183
|
+
RenderedFile(
|
|
184
|
+
"androidTest/java/org/kivyforge/test/KivyforgeServiceContractTest.java",
|
|
185
|
+
source,
|
|
186
|
+
)
|
|
187
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
5e27c39c2336c25748f279d8b105162d14b1a39eb7839d0b658432282d0ce79f gradle-wrapper.jar
|
|
Binary file
|