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
kivyforge/cli/init.py
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
"""``kivyforge init`` — smart project initialization (spec 05).
|
|
2
|
+
|
|
3
|
+
One write path:
|
|
4
|
+
|
|
5
|
+
* **update** — a ``pyproject.toml`` exists. Only add/replace the target
|
|
6
|
+
platform's ``[tool.kivy]`` + ``[tool.kivy.<platform>]``; ``[project]`` and
|
|
7
|
+
every other namespace are left untouched. No venv required.
|
|
8
|
+
|
|
9
|
+
Platform-aware: resolves the target the same way as every other verb
|
|
10
|
+
(``-p`` / ``KIVYFORGE_PLATFORM``), plus two init-only fallbacks that make sense
|
|
11
|
+
only when *configuring* a platform for the first time — see
|
|
12
|
+
``_resolve_init_platform``.
|
|
13
|
+
|
|
14
|
+
If ``requirements.txt`` is found but no ``pyproject.toml``, init exits non-zero
|
|
15
|
+
with a migration pointer rather than auto-migrating.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import platform as _platform_mod
|
|
22
|
+
import sys
|
|
23
|
+
import tomllib
|
|
24
|
+
from importlib import metadata
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
|
|
29
|
+
from ..config.model import (
|
|
30
|
+
DEFAULT_ANDROID_KEY_PASSWORD_ENV,
|
|
31
|
+
DEFAULT_ANDROID_STORE_PASSWORD_ENV,
|
|
32
|
+
DEFAULT_WINDOWS_STORE_SCOPE,
|
|
33
|
+
DEFAULT_WINDOWS_TIMESTAMP_URL,
|
|
34
|
+
AndroidSigningConfig,
|
|
35
|
+
MacosSigningConfig,
|
|
36
|
+
SigningConfig,
|
|
37
|
+
WindowsSigningConfig,
|
|
38
|
+
)
|
|
39
|
+
from ..platforms import PLATFORM_ENV_VAR, available_platform_names, get_platform
|
|
40
|
+
from ._common import PYPROJECT_NAME, ToolchainError
|
|
41
|
+
from ._platform import configured_platforms, platform_option
|
|
42
|
+
from .init_writer import (
|
|
43
|
+
append_block,
|
|
44
|
+
has_kivy_dep,
|
|
45
|
+
has_platform_overlay,
|
|
46
|
+
has_shared_table,
|
|
47
|
+
normalize_package_name,
|
|
48
|
+
render_android_tables,
|
|
49
|
+
render_kivy_tables,
|
|
50
|
+
render_linux_tables,
|
|
51
|
+
render_macos_tables,
|
|
52
|
+
render_windows_tables,
|
|
53
|
+
strip_platform_tables,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
REQUIREMENTS_NAME = "requirements.txt"
|
|
57
|
+
BUILDOZER_SPEC_NAME = "buildozer.spec"
|
|
58
|
+
|
|
59
|
+
# buildozer.spec `[app]` key -> the kivyforge key that replaces it. The value is
|
|
60
|
+
# the destination table and key; `None` means the setting has no counterpart
|
|
61
|
+
# because kivyforge does the thing differently (see _BUILDOZER_DROPPED).
|
|
62
|
+
_BUILDOZER_MAP: tuple[tuple[str, str], ...] = (
|
|
63
|
+
("title", "[tool.kivy].display_name"),
|
|
64
|
+
("package.name", "[tool.kivy.android].package (with package.domain)"),
|
|
65
|
+
("package.domain", "[tool.kivy.android].package (with package.name)"),
|
|
66
|
+
("version", "[project].version"),
|
|
67
|
+
("source.dir", "[tool.kivy].app_dir"),
|
|
68
|
+
("requirements", "[project].dependencies (as PEP 508 requirements)"),
|
|
69
|
+
("orientation", "[tool.kivy].orientation"),
|
|
70
|
+
("icon.filename", "[tool.kivy.android.icons].source"),
|
|
71
|
+
("presplash.filename", "[tool.kivy.android.splash].source"),
|
|
72
|
+
("android.api", "[tool.kivy.android].target_sdk / compile_sdk"),
|
|
73
|
+
("android.minapi", "[tool.kivy.android].min_sdk (kivyforge's floor is 24)"),
|
|
74
|
+
(
|
|
75
|
+
"android.archs",
|
|
76
|
+
"[tool.kivy.android].abis (arm64-v8a -> arm64_v8a; the 32-bit ABIs "
|
|
77
|
+
"armeabi-v7a and x86 are not supported)",
|
|
78
|
+
),
|
|
79
|
+
("android.permissions", "[tool.kivy.android.permissions].uses"),
|
|
80
|
+
("android.features", "[[tool.kivy.android.permissions.features]]"),
|
|
81
|
+
("services", "[[tool.kivy.android.services]]"),
|
|
82
|
+
("android.meta_data", "[tool.kivy.android.manifest].extra_application_xml"),
|
|
83
|
+
("android.add_activities", "[tool.kivy.android.manifest].extra_manifest_xml"),
|
|
84
|
+
("android.gradle_dependencies", "[tool.kivy.android].gradle_dependencies"),
|
|
85
|
+
("android.add_jars", "[[tool.kivy.android.android_libs]]"),
|
|
86
|
+
("android.release_artifact", "`kivyforge package -f apk|aab`"),
|
|
87
|
+
("android.debug_artifact", "`kivyforge build --debug -f apk|aab`"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Settings with no kivyforge counterpart, and why — these are the migration's
|
|
91
|
+
# real work, so name them rather than letting the user discover them one by one.
|
|
92
|
+
_BUILDOZER_DROPPED: tuple[tuple[str, str], ...] = (
|
|
93
|
+
(
|
|
94
|
+
"p4a.*, android.p4a_whitelist, android.blacklist_src",
|
|
95
|
+
"there is no python-for-android layer: dependencies are Android wheels "
|
|
96
|
+
"resolved by `kivyforge lock`.",
|
|
97
|
+
),
|
|
98
|
+
(
|
|
99
|
+
"requirements with a p4a recipe (e.g. 'kivy,pyjnius,numpy')",
|
|
100
|
+
"each becomes a normal requirement; anything compiled needs an Android "
|
|
101
|
+
"wheel (see docs/design/platforms/android/03).",
|
|
102
|
+
),
|
|
103
|
+
(
|
|
104
|
+
"android.ndk, android.sdk, android.ndk_api, android.gradle_version",
|
|
105
|
+
"kivyforge pins the toolchain itself; `kivyforge doctor -p android` "
|
|
106
|
+
"checks the host against those pins.",
|
|
107
|
+
),
|
|
108
|
+
(
|
|
109
|
+
"android.add_src",
|
|
110
|
+
"app-side Java is not a kivyforge concept; the bootstrap's Java is "
|
|
111
|
+
"generated, and `include_files` covers extra assets.",
|
|
112
|
+
),
|
|
113
|
+
(
|
|
114
|
+
"android.entrypoint, android.activity_class_name",
|
|
115
|
+
"the launcher is generated; `[tool.kivy].entry_point` names the Python "
|
|
116
|
+
"module to import instead.",
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
_BUILDOZER_MSG_HEAD = (
|
|
121
|
+
"buildozer.spec found but no pyproject.toml.\n"
|
|
122
|
+
" kivyforge is configured through pyproject.toml and will not migrate a\n"
|
|
123
|
+
" buildozer.spec automatically. Create one with your app's metadata, then\n"
|
|
124
|
+
" re-run `kivyforge init -p android`.\n"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
_REQUIREMENTS_MSG = (
|
|
128
|
+
"requirements.txt found but no pyproject.toml.\n"
|
|
129
|
+
" kivyforge requires pyproject.toml — it will not migrate requirements.txt\n"
|
|
130
|
+
" automatically. Transfer your dependencies to a new pyproject.toml:\n\n"
|
|
131
|
+
" [project]\n"
|
|
132
|
+
' name = "myapp" # your app name\n'
|
|
133
|
+
' version = "0.1.0"\n'
|
|
134
|
+
" dependencies = [\n"
|
|
135
|
+
' "kivy>=3.0",\n'
|
|
136
|
+
" # ... paste your other requirements here\n"
|
|
137
|
+
" ]\n\n"
|
|
138
|
+
" Then re-run kivyforge init."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@click.command()
|
|
143
|
+
@platform_option
|
|
144
|
+
@click.option(
|
|
145
|
+
"--force", is_flag=True, help="Regenerate [tool.kivy*] (preserves signing)."
|
|
146
|
+
)
|
|
147
|
+
def init(cli_platform: str | None, force: bool) -> None:
|
|
148
|
+
"""Seed [tool.kivy] + the target platform's overlay into pyproject.toml."""
|
|
149
|
+
cwd = Path.cwd()
|
|
150
|
+
pyproject = cwd / PYPROJECT_NAME
|
|
151
|
+
requirements = cwd / REQUIREMENTS_NAME
|
|
152
|
+
buildozer_spec = cwd / BUILDOZER_SPEC_NAME
|
|
153
|
+
|
|
154
|
+
if pyproject.is_file():
|
|
155
|
+
platform_name = _resolve_init_platform(cli_platform, pyproject)
|
|
156
|
+
_run_update_path(pyproject, force=force, platform_name=platform_name)
|
|
157
|
+
elif buildozer_spec.is_file():
|
|
158
|
+
raise ToolchainError(_buildozer_migration_message(buildozer_spec))
|
|
159
|
+
elif requirements.is_file():
|
|
160
|
+
raise ToolchainError(_REQUIREMENTS_MSG)
|
|
161
|
+
else:
|
|
162
|
+
raise ToolchainError(
|
|
163
|
+
f"no {PYPROJECT_NAME} found in {cwd}.\n"
|
|
164
|
+
f" Create a minimal {PYPROJECT_NAME} with your app's metadata, then re-run:\n\n"
|
|
165
|
+
f" [project]\n"
|
|
166
|
+
f' name = "myapp"\n'
|
|
167
|
+
f' version = "0.1.0"\n'
|
|
168
|
+
f" dependencies = [\n"
|
|
169
|
+
f' "kivy>=3.0",\n'
|
|
170
|
+
f" ]\n\n"
|
|
171
|
+
f" See https://packaging.python.org/tutorials/packaging-projects/ for details."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _read_buildozer_spec(path: Path) -> dict[str, str]:
|
|
176
|
+
"""The ``[app]`` section as a flat dict, or ``{}`` if it will not parse.
|
|
177
|
+
|
|
178
|
+
buildozer.spec is INI, but a hand-edited one need not be valid, and this is
|
|
179
|
+
an error path — a spec we cannot read still gets the generic key mapping.
|
|
180
|
+
"""
|
|
181
|
+
import configparser
|
|
182
|
+
|
|
183
|
+
# Raw: buildozer.spec uses %(source.dir)s-style interpolation that means
|
|
184
|
+
# something to buildozer and nothing here, and utf-8-sig because an editor
|
|
185
|
+
# that BOMs the file would otherwise hide the section header.
|
|
186
|
+
parser = configparser.RawConfigParser(strict=False)
|
|
187
|
+
try:
|
|
188
|
+
parser.read(path, encoding="utf-8-sig")
|
|
189
|
+
if not parser.has_section("app"):
|
|
190
|
+
return {}
|
|
191
|
+
return {key: value.strip() for key, value in parser.items("app")}
|
|
192
|
+
except (configparser.Error, OSError, UnicodeDecodeError):
|
|
193
|
+
return {}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _buildozer_migration_message(path: Path) -> str:
|
|
197
|
+
"""The non-zero exit's migration pointer (android/06 §init).
|
|
198
|
+
|
|
199
|
+
Echoes the spec's own values next to their kivyforge keys where it can:
|
|
200
|
+
a mapping table the user has to re-read against their file is busywork, and
|
|
201
|
+
the values are right there.
|
|
202
|
+
"""
|
|
203
|
+
spec = _read_buildozer_spec(path)
|
|
204
|
+
lines = [_BUILDOZER_MSG_HEAD, " Your buildozer.spec maps onto these keys:\n"]
|
|
205
|
+
width = max(len(key) for key, _ in _BUILDOZER_MAP)
|
|
206
|
+
for key, target in _BUILDOZER_MAP:
|
|
207
|
+
value = spec.get(key)
|
|
208
|
+
shown = f" {key.ljust(width)} -> {target}"
|
|
209
|
+
if value:
|
|
210
|
+
shown += f"\n {' ' * width} currently: {_ellipsize(value)}"
|
|
211
|
+
lines.append(shown)
|
|
212
|
+
lines.append("\n No kivyforge counterpart:\n")
|
|
213
|
+
for keys, why in _BUILDOZER_DROPPED:
|
|
214
|
+
lines.append(f" {keys}\n {why}")
|
|
215
|
+
lines.append(
|
|
216
|
+
"\n Full reference: docs/design/platforms/android/01-pyproject-android.md"
|
|
217
|
+
)
|
|
218
|
+
return "\n".join(lines)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _ellipsize(value: str, limit: int = 70) -> str:
|
|
222
|
+
flat = " ".join(value.split())
|
|
223
|
+
return flat if len(flat) <= limit else flat[: limit - 3] + "..."
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _resolve_init_platform(
|
|
227
|
+
cli_platform: str | None,
|
|
228
|
+
pyproject: Path,
|
|
229
|
+
*,
|
|
230
|
+
host_system: str | None = None,
|
|
231
|
+
) -> str:
|
|
232
|
+
"""Resolve init's target platform.
|
|
233
|
+
|
|
234
|
+
Same top two steps as every other verb (``-p`` then ``KIVYFORGE_PLATFORM``),
|
|
235
|
+
but init additionally needs to handle the case it alone faces: *configuring*
|
|
236
|
+
a platform for the first time, when nothing is "configured" yet. So instead
|
|
237
|
+
of the shared ``resolve_target`` (whose host-default step requires the
|
|
238
|
+
platform to already be configured — the right call for build/run/etc., but
|
|
239
|
+
backwards for init), this:
|
|
240
|
+
|
|
241
|
+
1. ``--platform`` / ``-p``.
|
|
242
|
+
2. ``KIVYFORGE_PLATFORM``.
|
|
243
|
+
3. The project's one already-configured overlay, if exactly one exists
|
|
244
|
+
(the ``--force``-regenerate case — no flag needed to update what's
|
|
245
|
+
already there).
|
|
246
|
+
4. The host OS's own platform (unconditionally — this *is* how it gets
|
|
247
|
+
configured the first time). iOS never matches here (no host maps to
|
|
248
|
+
it); it always needs an explicit choice, same as everywhere else.
|
|
249
|
+
|
|
250
|
+
``host_system`` is injectable (like the shared ``resolve_target``'s) so
|
|
251
|
+
tests aren't at the mercy of the machine they happen to run on; it
|
|
252
|
+
defaults to the real host (``platform.system()``).
|
|
253
|
+
"""
|
|
254
|
+
if cli_platform:
|
|
255
|
+
return cli_platform
|
|
256
|
+
|
|
257
|
+
env_platform = os.environ.get(PLATFORM_ENV_VAR)
|
|
258
|
+
if env_platform:
|
|
259
|
+
if env_platform not in available_platform_names():
|
|
260
|
+
raise ToolchainError(
|
|
261
|
+
f"unknown platform {env_platform!r} in ${PLATFORM_ENV_VAR}; "
|
|
262
|
+
f"registered: {', '.join(available_platform_names())}"
|
|
263
|
+
)
|
|
264
|
+
return env_platform
|
|
265
|
+
|
|
266
|
+
configured = configured_platforms(pyproject)
|
|
267
|
+
if len(configured) == 1:
|
|
268
|
+
return next(iter(configured))
|
|
269
|
+
if len(configured) > 1:
|
|
270
|
+
example = sorted(configured)[0]
|
|
271
|
+
raise ToolchainError(
|
|
272
|
+
"multiple platforms configured in this pyproject.toml "
|
|
273
|
+
f"({', '.join(sorted(configured))}); pass one explicitly:\n"
|
|
274
|
+
f" kivyforge init --platform {example}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
host = host_system if host_system is not None else _platform_mod.system()
|
|
278
|
+
for name in available_platform_names():
|
|
279
|
+
if get_platform(name).host_system == host:
|
|
280
|
+
return name
|
|
281
|
+
|
|
282
|
+
raise ToolchainError(
|
|
283
|
+
"cannot infer a target platform on this host.\n"
|
|
284
|
+
" Pass one explicitly: kivyforge init --platform macos\n"
|
|
285
|
+
f" Or set a session default: export {PLATFORM_ENV_VAR}=macos"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _run_update_path(pyproject: Path, *, force: bool, platform_name: str) -> None:
|
|
290
|
+
text = pyproject.read_text(encoding="utf-8")
|
|
291
|
+
raw = _safe_parse(text, pyproject)
|
|
292
|
+
|
|
293
|
+
if "project" not in raw:
|
|
294
|
+
raise ToolchainError(
|
|
295
|
+
f"{PYPROJECT_NAME} has no [project] table.\n"
|
|
296
|
+
" init updates only [tool.kivy*]; it will not author [project] for "
|
|
297
|
+
"an existing file. Add a minimal [project] (name + version) first."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
table_key = f"tool.kivy.{platform_name}"
|
|
301
|
+
existing_overlay = has_platform_overlay(text, platform_name)
|
|
302
|
+
if existing_overlay and not force:
|
|
303
|
+
raise ToolchainError(
|
|
304
|
+
f"[{table_key}] already exists. Re-run with --force to regenerate "
|
|
305
|
+
f"it (your [{table_key}.signing] is preserved)."
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
app_slug = _project_slug(raw)
|
|
309
|
+
deps = raw.get("project", {}).get("dependencies", [])
|
|
310
|
+
kivy = has_kivy_dep(deps) if isinstance(deps, list) else False
|
|
311
|
+
include_shared = not has_shared_table(text)
|
|
312
|
+
table = _platform_table(raw, platform_name)
|
|
313
|
+
|
|
314
|
+
if existing_overlay and force:
|
|
315
|
+
stripped = strip_platform_tables(text, platform_name)
|
|
316
|
+
block = _render_overlay(
|
|
317
|
+
platform_name,
|
|
318
|
+
app_slug,
|
|
319
|
+
table,
|
|
320
|
+
has_kivy=kivy,
|
|
321
|
+
include_shared=include_shared,
|
|
322
|
+
preserve=True,
|
|
323
|
+
)
|
|
324
|
+
new_text = append_block(stripped, block)
|
|
325
|
+
pyproject.write_text(new_text, encoding="utf-8")
|
|
326
|
+
click.echo(
|
|
327
|
+
f"Regenerated [{table_key}] (signing/python/icon settings preserved)."
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
block = _render_overlay(
|
|
331
|
+
platform_name,
|
|
332
|
+
app_slug,
|
|
333
|
+
table,
|
|
334
|
+
has_kivy=kivy,
|
|
335
|
+
include_shared=include_shared,
|
|
336
|
+
preserve=False,
|
|
337
|
+
)
|
|
338
|
+
new_text = append_block(text, block)
|
|
339
|
+
pyproject.write_text(new_text, encoding="utf-8")
|
|
340
|
+
added = (
|
|
341
|
+
f"[{table_key}]" if not include_shared else f"[tool.kivy] + [{table_key}]"
|
|
342
|
+
)
|
|
343
|
+
click.echo(f"Added {added} to pyproject.toml.")
|
|
344
|
+
|
|
345
|
+
_maybe_warn_drift(raw)
|
|
346
|
+
click.echo(
|
|
347
|
+
f"Next: fill in the TODOs in [{table_key}], then `kivyforge lock -p {platform_name}`."
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _render_overlay(
|
|
352
|
+
platform_name: str,
|
|
353
|
+
app_slug: str,
|
|
354
|
+
table: dict,
|
|
355
|
+
*,
|
|
356
|
+
has_kivy: bool,
|
|
357
|
+
include_shared: bool,
|
|
358
|
+
preserve: bool,
|
|
359
|
+
) -> str:
|
|
360
|
+
"""Render the (optional) shared ``[tool.kivy]`` + the target platform's overlay."""
|
|
361
|
+
if platform_name == "ios":
|
|
362
|
+
splash_source, splash_background = (
|
|
363
|
+
_read_splash(table) if preserve else (None, None)
|
|
364
|
+
)
|
|
365
|
+
return render_kivy_tables(
|
|
366
|
+
app_slug,
|
|
367
|
+
signing=_read_signing(table) if preserve else None,
|
|
368
|
+
python_version=_read_python_version(table) if preserve else None,
|
|
369
|
+
has_kivy=has_kivy,
|
|
370
|
+
simulator_archs=_read_str_list(table, "simulator_archs")
|
|
371
|
+
if preserve
|
|
372
|
+
else None,
|
|
373
|
+
icon_source=_read_icon_source(table) if preserve else None,
|
|
374
|
+
splash_source=splash_source,
|
|
375
|
+
splash_background=splash_background,
|
|
376
|
+
include_shared=include_shared,
|
|
377
|
+
)
|
|
378
|
+
if platform_name == "macos":
|
|
379
|
+
return render_macos_tables(
|
|
380
|
+
app_slug,
|
|
381
|
+
signing=_read_macos_signing(table) if preserve else None,
|
|
382
|
+
python_version=_read_python_version(table) if preserve else None,
|
|
383
|
+
has_kivy=has_kivy,
|
|
384
|
+
archs=_read_str_list(table, "archs") if preserve else None,
|
|
385
|
+
icon_source=_read_icon_source(table) if preserve else None,
|
|
386
|
+
include_shared=include_shared,
|
|
387
|
+
)
|
|
388
|
+
if platform_name == "linux":
|
|
389
|
+
return render_linux_tables(
|
|
390
|
+
app_slug,
|
|
391
|
+
python_version=_read_python_version(table) if preserve else None,
|
|
392
|
+
has_kivy=has_kivy,
|
|
393
|
+
archs=_read_str_list(table, "archs") if preserve else None,
|
|
394
|
+
icon_source=_read_icon_source(table) if preserve else None,
|
|
395
|
+
categories=_read_categories(table) if preserve else None,
|
|
396
|
+
include_shared=include_shared,
|
|
397
|
+
)
|
|
398
|
+
if platform_name == "windows":
|
|
399
|
+
return render_windows_tables(
|
|
400
|
+
app_slug,
|
|
401
|
+
signing=_read_windows_signing(table) if preserve else None,
|
|
402
|
+
python_version=_read_python_version(table) if preserve else None,
|
|
403
|
+
has_kivy=has_kivy,
|
|
404
|
+
archs=_read_str_list(table, "archs") if preserve else None,
|
|
405
|
+
icon_source=_read_icon_source(table) if preserve else None,
|
|
406
|
+
include_shared=include_shared,
|
|
407
|
+
)
|
|
408
|
+
if platform_name == "android":
|
|
409
|
+
splash_source, splash_background = (
|
|
410
|
+
_read_splash(table) if preserve else (None, None)
|
|
411
|
+
)
|
|
412
|
+
package = table.get("package") if preserve else None
|
|
413
|
+
kivy_generation = table.get("kivy_generation") if preserve else None
|
|
414
|
+
return render_android_tables(
|
|
415
|
+
app_slug,
|
|
416
|
+
signing=_read_android_signing(table) if preserve else None,
|
|
417
|
+
python_version=_read_python_version(table) if preserve else None,
|
|
418
|
+
has_kivy=has_kivy,
|
|
419
|
+
package=package if isinstance(package, str) and package else None,
|
|
420
|
+
abis=_read_str_list(table, "abis") if preserve else None,
|
|
421
|
+
kivy_generation=kivy_generation
|
|
422
|
+
if isinstance(kivy_generation, int)
|
|
423
|
+
and not isinstance(kivy_generation, bool)
|
|
424
|
+
else None,
|
|
425
|
+
icon_source=_read_icon_source(table) if preserve else None,
|
|
426
|
+
splash_source=splash_source,
|
|
427
|
+
splash_background=splash_background,
|
|
428
|
+
include_shared=include_shared,
|
|
429
|
+
)
|
|
430
|
+
raise ToolchainError(
|
|
431
|
+
f"`kivyforge init` does not support platform {platform_name!r} yet."
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# --------------------------------------------------------------------------- #
|
|
436
|
+
# environment probing
|
|
437
|
+
# --------------------------------------------------------------------------- #
|
|
438
|
+
def _venv_active() -> bool:
|
|
439
|
+
return sys.prefix != sys.base_prefix
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _installed_versions() -> dict[str, str]:
|
|
443
|
+
out: dict[str, str] = {}
|
|
444
|
+
for dist in metadata.distributions():
|
|
445
|
+
name = dist.metadata["Name"]
|
|
446
|
+
if name:
|
|
447
|
+
out[_canon(name)] = dist.version
|
|
448
|
+
return out
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _canon(name: str) -> str:
|
|
452
|
+
import re
|
|
453
|
+
|
|
454
|
+
return re.sub(r"[-_.]+", "-", name).lower()
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _safe_parse(text: str, path: Path) -> dict:
|
|
458
|
+
try:
|
|
459
|
+
return tomllib.loads(text)
|
|
460
|
+
except tomllib.TOMLDecodeError as exc:
|
|
461
|
+
raise ToolchainError(f"{path} is not valid TOML: {exc}") from exc
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _project_slug(raw: dict) -> str:
|
|
465
|
+
name = raw.get("project", {}).get("name")
|
|
466
|
+
if isinstance(name, str) and name:
|
|
467
|
+
return normalize_package_name(name)
|
|
468
|
+
return "app"
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _platform_table(raw: dict, platform_name: str) -> dict:
|
|
472
|
+
try:
|
|
473
|
+
table = raw["tool"]["kivy"][platform_name]
|
|
474
|
+
except (KeyError, TypeError):
|
|
475
|
+
return {}
|
|
476
|
+
return table if isinstance(table, dict) else {}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _read_python_version(table: dict) -> str | None:
|
|
480
|
+
try:
|
|
481
|
+
version = table["python"]["version"]
|
|
482
|
+
except (KeyError, TypeError):
|
|
483
|
+
return None
|
|
484
|
+
return version if isinstance(version, str) and version else None
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _read_str_list(table: dict, key: str) -> list[str] | None:
|
|
488
|
+
"""Return a user-set list-of-strings field, or None when absent/invalid.
|
|
489
|
+
|
|
490
|
+
None preserves the commented stub on ``--force`` so the default still holds.
|
|
491
|
+
"""
|
|
492
|
+
values = table.get(key)
|
|
493
|
+
if isinstance(values, list) and values and all(isinstance(v, str) for v in values):
|
|
494
|
+
return values
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _read_icon_source(table: dict) -> str | None:
|
|
499
|
+
icons = table.get("icons")
|
|
500
|
+
if isinstance(icons, dict):
|
|
501
|
+
source = icons.get("source")
|
|
502
|
+
if isinstance(source, str) and source:
|
|
503
|
+
return source
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _read_splash(table: dict) -> tuple[str | None, str | None]:
|
|
508
|
+
splash = table.get("splash")
|
|
509
|
+
if not isinstance(splash, dict):
|
|
510
|
+
return None, None
|
|
511
|
+
source = splash.get("source")
|
|
512
|
+
background = splash.get("background")
|
|
513
|
+
return (
|
|
514
|
+
source if isinstance(source, str) and source else None,
|
|
515
|
+
background if isinstance(background, str) and background else None,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _read_signing(table: dict) -> SigningConfig | None:
|
|
520
|
+
signing = table.get("signing")
|
|
521
|
+
if not isinstance(signing, dict):
|
|
522
|
+
return None
|
|
523
|
+
return SigningConfig(
|
|
524
|
+
team_id=signing.get("team_id", ""),
|
|
525
|
+
identity=signing.get("identity", "Apple Development"),
|
|
526
|
+
provisioning_profile=signing.get("provisioning_profile", ""),
|
|
527
|
+
auto_signing=bool(signing.get("auto_signing", True)),
|
|
528
|
+
upload_symbols=bool(signing.get("upload_symbols", True)),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _read_macos_signing(table: dict) -> MacosSigningConfig | None:
|
|
533
|
+
signing = table.get("signing")
|
|
534
|
+
if not isinstance(signing, dict):
|
|
535
|
+
return None
|
|
536
|
+
identity = signing.get("identity", "")
|
|
537
|
+
if not identity:
|
|
538
|
+
return None
|
|
539
|
+
return MacosSigningConfig(
|
|
540
|
+
identity=identity,
|
|
541
|
+
team_id=signing.get("team_id", ""),
|
|
542
|
+
notary_profile=signing.get("notary_profile", ""),
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _read_windows_signing(table: dict) -> WindowsSigningConfig | None:
|
|
547
|
+
signing = table.get("signing")
|
|
548
|
+
if not isinstance(signing, dict):
|
|
549
|
+
return None
|
|
550
|
+
thumbprint = signing.get("thumbprint", "")
|
|
551
|
+
if not thumbprint:
|
|
552
|
+
return None
|
|
553
|
+
return WindowsSigningConfig(
|
|
554
|
+
thumbprint=thumbprint,
|
|
555
|
+
timestamp_url=signing.get("timestamp_url", DEFAULT_WINDOWS_TIMESTAMP_URL),
|
|
556
|
+
store_scope=signing.get("store_scope", DEFAULT_WINDOWS_STORE_SCOPE),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _read_android_signing(table: dict) -> AndroidSigningConfig | None:
|
|
561
|
+
signing = table.get("signing")
|
|
562
|
+
if not isinstance(signing, dict):
|
|
563
|
+
return None
|
|
564
|
+
keystore = signing.get("keystore", "")
|
|
565
|
+
if not keystore:
|
|
566
|
+
return None
|
|
567
|
+
return AndroidSigningConfig(
|
|
568
|
+
keystore=keystore,
|
|
569
|
+
key_alias=signing.get("key_alias", ""),
|
|
570
|
+
store_password_env=signing.get(
|
|
571
|
+
"store_password_env", DEFAULT_ANDROID_STORE_PASSWORD_ENV
|
|
572
|
+
),
|
|
573
|
+
key_password_env=signing.get(
|
|
574
|
+
"key_password_env", DEFAULT_ANDROID_KEY_PASSWORD_ENV
|
|
575
|
+
),
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _read_categories(table: dict) -> list[str] | None:
|
|
580
|
+
desktop = table.get("desktop")
|
|
581
|
+
if not isinstance(desktop, dict):
|
|
582
|
+
return None
|
|
583
|
+
return _read_str_list(desktop, "categories")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _maybe_warn_drift(raw: dict) -> None:
|
|
587
|
+
"""If a venv is active, warn when an installed dep drifts from its specifier."""
|
|
588
|
+
if not _venv_active():
|
|
589
|
+
return
|
|
590
|
+
deps = raw.get("project", {}).get("dependencies", [])
|
|
591
|
+
if not isinstance(deps, list):
|
|
592
|
+
return
|
|
593
|
+
installed = _installed_versions()
|
|
594
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
595
|
+
from packaging.version import Version
|
|
596
|
+
|
|
597
|
+
for raw_req in deps:
|
|
598
|
+
try:
|
|
599
|
+
req = Requirement(raw_req)
|
|
600
|
+
except InvalidRequirement:
|
|
601
|
+
continue
|
|
602
|
+
version = installed.get(_canon(req.name))
|
|
603
|
+
if (
|
|
604
|
+
version
|
|
605
|
+
and str(req.specifier)
|
|
606
|
+
and not req.specifier.contains(Version(version), prereleases=True)
|
|
607
|
+
):
|
|
608
|
+
click.echo(
|
|
609
|
+
f"warning: installed {req.name} {version} is outside declared "
|
|
610
|
+
f"'{req.specifier}'. (Not modified.)",
|
|
611
|
+
err=True,
|
|
612
|
+
)
|