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,298 @@
|
|
|
1
|
+
r"""Assemble the Windows onedir bundle (windows-spec).
|
|
2
|
+
|
|
3
|
+
Orchestrates the stages: pick the assembly arch from the lock (+ ``--arch``),
|
|
4
|
+
stage the whole runtime prefix, install the locked wheels into it (no pip),
|
|
5
|
+
copy the app sources, stage any declared native binaries, generate the bootstrap
|
|
6
|
+
module, then place + resource-patch the launcher. Produces:
|
|
7
|
+
|
|
8
|
+
build/windows/<Name>/
|
|
9
|
+
├── <Name>.exe ← prebuilt launcher, resource-patched per app
|
|
10
|
+
├── _kivyforge_bootstrap.py ← generated (fixed name)
|
|
11
|
+
├── app/ ← user code ([tool.kivy].app_dir)
|
|
12
|
+
├── bin/ ← declared native binaries (absent when empty)
|
|
13
|
+
└── python/ ← WHOLE PBS prefix
|
|
14
|
+
|
|
15
|
+
The tree is assembled **in place** (see :mod:`.fsswap`): a freshly written tree
|
|
16
|
+
cannot be reliably renamed on Windows because the antivirus scanner holds new
|
|
17
|
+
files open, so any previous bundle is reserved first and restored if assembly
|
|
18
|
+
fails — a broken build never destroys a working one.
|
|
19
|
+
|
|
20
|
+
The onedir *folder* is a distributable format either way (portable use, zip it
|
|
21
|
+
up yourself) — but *this* build's contents depend on ``release``: ``build``/
|
|
22
|
+
``run`` (``release=False``) keep readable ``.py`` for fast iteration, while
|
|
23
|
+
``package`` (``release=True``) additionally byte-compiles per
|
|
24
|
+
``[tool.kivy.windows.build_settings]`` before copying the tree into
|
|
25
|
+
``dist/windows``. ``release`` is a build-policy flag, not a statement about
|
|
26
|
+
whether the folder *can* be distributed.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import platform
|
|
32
|
+
import shutil
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
import click
|
|
36
|
+
|
|
37
|
+
from kivyforge.artifacts.cache import ArtifactCache
|
|
38
|
+
from kivyforge.bundle.pycompile import PycompileError, byte_compile, select_compiler
|
|
39
|
+
from kivyforge.config.model import Config
|
|
40
|
+
|
|
41
|
+
from . import WindowsBundleError
|
|
42
|
+
from .fsswap import discard_reserved, reserve_previous, restore_previous
|
|
43
|
+
from .icons import stage_icon
|
|
44
|
+
from .launcher import launcher_exe_name, place_launcher, write_bootstrap
|
|
45
|
+
from .lock import WindowsLockfile
|
|
46
|
+
from .naming import windows_safe_name
|
|
47
|
+
from .native_stage import stage_native_binaries
|
|
48
|
+
from .rcedit import ResourcePatch
|
|
49
|
+
from .runtime_stage import stage_runtime
|
|
50
|
+
from .signing import pep440_to_file_version
|
|
51
|
+
from .wheels_stage import stage_wheels
|
|
52
|
+
|
|
53
|
+
_IGNORE = shutil.ignore_patterns("__pycache__", "*.pyc")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def resolve_assembly_arch(locked: tuple[str, ...], arch: str | None) -> str:
|
|
57
|
+
"""The single arch to assemble. Windows ships one arch (``amd64``) per onedir."""
|
|
58
|
+
if not locked:
|
|
59
|
+
raise WindowsBundleError(
|
|
60
|
+
"the lock covers no architectures; re-run `kivyforge lock -p windows`."
|
|
61
|
+
)
|
|
62
|
+
if arch is None:
|
|
63
|
+
return locked[0]
|
|
64
|
+
if arch not in locked:
|
|
65
|
+
raise WindowsBundleError(
|
|
66
|
+
f"--arch {arch} is not in the lock (covers {', '.join(locked)}).\n"
|
|
67
|
+
f" Add {arch!r} to [tool.kivy.windows].archs and re-lock, or pick a "
|
|
68
|
+
"locked arch."
|
|
69
|
+
)
|
|
70
|
+
return arch
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bundle_dir_name(config: Config) -> str:
|
|
74
|
+
"""The onedir folder name for *config* (Windows-safe display name)."""
|
|
75
|
+
return windows_safe_name(config.display_name)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _setting_applies(value: bool | str, *, release: bool) -> bool:
|
|
79
|
+
"""A ``build_settings`` tri-state resolved for the build being produced."""
|
|
80
|
+
if isinstance(value, bool):
|
|
81
|
+
return value
|
|
82
|
+
return release # DESKTOP_RELEASE_ONLY
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _resolve_byte_compile(
|
|
86
|
+
config: Config,
|
|
87
|
+
*,
|
|
88
|
+
staged_interpreter: Path,
|
|
89
|
+
target_arch: str,
|
|
90
|
+
python_version: str,
|
|
91
|
+
release: bool,
|
|
92
|
+
) -> tuple[tuple[str, ...] | None, bool]:
|
|
93
|
+
"""Decide whether to byte-compile, and with which interpreter.
|
|
94
|
+
|
|
95
|
+
Returns ``(compiler_argv_or_None, strip_source)``; ``None`` means do not
|
|
96
|
+
byte-compile. ``byte_compile = true`` is read as "I insist", so a missing
|
|
97
|
+
interpreter is an error; the default ``"release"`` degrades to shipping
|
|
98
|
+
source with a warning rather than breaking a build the user never
|
|
99
|
+
configured.
|
|
100
|
+
"""
|
|
101
|
+
settings = config.windows_required.build_settings
|
|
102
|
+
if not _setting_applies(settings.byte_compile, release=release):
|
|
103
|
+
return None, False
|
|
104
|
+
native = platform.machine().lower() == target_arch
|
|
105
|
+
compiler = select_compiler(
|
|
106
|
+
staged_interpreter=staged_interpreter,
|
|
107
|
+
native=native,
|
|
108
|
+
python_version=python_version,
|
|
109
|
+
)
|
|
110
|
+
if compiler is None:
|
|
111
|
+
message = (
|
|
112
|
+
f"this project ships CPython {python_version}, and no CPython of "
|
|
113
|
+
"that minor could be found to byte-compile with (a .pyc is only "
|
|
114
|
+
"loadable by the exact CPython minor that wrote it)"
|
|
115
|
+
)
|
|
116
|
+
if settings.byte_compile is True:
|
|
117
|
+
raise WindowsBundleError(
|
|
118
|
+
"[tool.kivy.windows.build_settings].byte_compile = true but "
|
|
119
|
+
f"{message}.\n"
|
|
120
|
+
" Install a matching CPython, or set byte_compile = false."
|
|
121
|
+
)
|
|
122
|
+
click.echo(f"[stage] not byte-compiling: {message}.")
|
|
123
|
+
return None, False
|
|
124
|
+
strip = _setting_applies(settings.strip_source, release=release)
|
|
125
|
+
return compiler, strip
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_onedir(
|
|
129
|
+
config: Config,
|
|
130
|
+
lock: WindowsLockfile,
|
|
131
|
+
project_root: Path,
|
|
132
|
+
*,
|
|
133
|
+
arch: str | None = None,
|
|
134
|
+
staging_dir: Path | None = None,
|
|
135
|
+
no_cache: bool = False,
|
|
136
|
+
cache: ArtifactCache | None = None,
|
|
137
|
+
release: bool = False,
|
|
138
|
+
echo=click.echo,
|
|
139
|
+
) -> Path:
|
|
140
|
+
"""Build the onedir bundle tree and return its path.
|
|
141
|
+
|
|
142
|
+
The tree is assembled **directly into its final location**. Temp-dir + rename
|
|
143
|
+
(the POSIX pattern) is not viable on Windows: a just-written tree cannot be
|
|
144
|
+
reliably renamed because the antivirus real-time scanner holds the new
|
|
145
|
+
executables/DLLs open — on a cold first-sight scan (cloud lookup) that lasts
|
|
146
|
+
well beyond any reasonable retry window. Instead we preserve any previous
|
|
147
|
+
bundle in a trash dir first and restore it if assembly fails, so a broken
|
|
148
|
+
build never destroys a working one. The trash is an already-scanned tree,
|
|
149
|
+
which renames instantly; only freshly written files (which we never rename)
|
|
150
|
+
hit the scan window.
|
|
151
|
+
"""
|
|
152
|
+
target_arch = resolve_assembly_arch(lock.archs, arch)
|
|
153
|
+
cache = cache or ArtifactCache()
|
|
154
|
+
staging_dir = staging_dir or (project_root / "build" / "windows")
|
|
155
|
+
bundle = staging_dir / bundle_dir_name(config)
|
|
156
|
+
|
|
157
|
+
staging_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
trash = reserve_previous(bundle)
|
|
159
|
+
try:
|
|
160
|
+
echo(
|
|
161
|
+
f"Staging CPython {lock.python_runtime.version} runtime ({target_arch}) ..."
|
|
162
|
+
)
|
|
163
|
+
stage_runtime(
|
|
164
|
+
lock.python_runtime,
|
|
165
|
+
target_arch,
|
|
166
|
+
bundle / "python",
|
|
167
|
+
project_root=project_root,
|
|
168
|
+
cache=cache,
|
|
169
|
+
no_cache=no_cache,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
echo(f"Installing {len(lock.packages)} locked packages ...")
|
|
173
|
+
stage_wheels(
|
|
174
|
+
lock.packages,
|
|
175
|
+
target_arch,
|
|
176
|
+
bundle / "python",
|
|
177
|
+
project_root=project_root,
|
|
178
|
+
cache=cache,
|
|
179
|
+
no_cache=no_cache,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if lock.native_binaries:
|
|
183
|
+
echo(f"Staging {len(lock.native_binaries)} native binaries ...")
|
|
184
|
+
stage_native_binaries(
|
|
185
|
+
lock,
|
|
186
|
+
bundle,
|
|
187
|
+
target_arch,
|
|
188
|
+
project_root=project_root,
|
|
189
|
+
cache=cache,
|
|
190
|
+
no_cache=no_cache,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
_copy_app_sources(config, project_root, bundle / "app")
|
|
194
|
+
|
|
195
|
+
compiler, strip_source = _resolve_byte_compile(
|
|
196
|
+
config,
|
|
197
|
+
staged_interpreter=bundle / "python" / "python.exe",
|
|
198
|
+
target_arch=target_arch,
|
|
199
|
+
python_version=lock.python_runtime.version,
|
|
200
|
+
release=release,
|
|
201
|
+
)
|
|
202
|
+
if compiler is not None:
|
|
203
|
+
with_what = " ".join(compiler) if compiler else "this interpreter"
|
|
204
|
+
echo(
|
|
205
|
+
f"[stage] byte-compiling the Python payload with {with_what}"
|
|
206
|
+
+ (" (.pyc only)" if strip_source else "")
|
|
207
|
+
)
|
|
208
|
+
try:
|
|
209
|
+
byte_compile(
|
|
210
|
+
[bundle / "app", bundle / "python" / "Lib" / "site-packages"],
|
|
211
|
+
compiler=compiler,
|
|
212
|
+
strip_source=strip_source,
|
|
213
|
+
stripdir=bundle,
|
|
214
|
+
)
|
|
215
|
+
except PycompileError as exc:
|
|
216
|
+
raise WindowsBundleError(
|
|
217
|
+
f"{exc}\n Fix it, or set "
|
|
218
|
+
"[tool.kivy.windows.build_settings].byte_compile = false."
|
|
219
|
+
) from exc
|
|
220
|
+
|
|
221
|
+
write_bootstrap(
|
|
222
|
+
bundle,
|
|
223
|
+
entry_point=config.kivy.entry_point,
|
|
224
|
+
app_id=config.windows_required.app_id,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
icon = stage_icon(config, project_root, bundle / "app.ico")
|
|
228
|
+
place_launcher(
|
|
229
|
+
bundle,
|
|
230
|
+
display_name=config.display_name,
|
|
231
|
+
patch=_resource_patch(config, icon),
|
|
232
|
+
)
|
|
233
|
+
except BaseException:
|
|
234
|
+
shutil.rmtree(bundle, ignore_errors=True)
|
|
235
|
+
restore_previous(trash, bundle)
|
|
236
|
+
raise
|
|
237
|
+
|
|
238
|
+
discard_reserved(trash)
|
|
239
|
+
return bundle
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _first_author(config: Config) -> str | None:
|
|
243
|
+
"""First ``[project].authors`` name, or ``None`` — the publisher/copyright.
|
|
244
|
+
|
|
245
|
+
Mirrors the iOS ``NSHumanReadableCopyright`` convention (pyproject-ios): the
|
|
246
|
+
first author's name populates both ``LegalCopyright`` and ``CompanyName`` in
|
|
247
|
+
the Windows version resource. Verbatim (no synthesized year) so the patch
|
|
248
|
+
stays deterministic.
|
|
249
|
+
"""
|
|
250
|
+
return next((a.name for a in config.project.authors if a.name), None)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _resource_patch(config: Config, icon: Path | None) -> ResourcePatch:
|
|
254
|
+
"""The launcher resource patch from project metadata.
|
|
255
|
+
|
|
256
|
+
The string ProductVersion carries the full PEP 440 version; the numeric
|
|
257
|
+
FILEVERSION/PRODUCTVERSION use the deterministic four-part mapping (signing).
|
|
258
|
+
LegalCopyright/CompanyName come from the first ``[project].authors`` entry.
|
|
259
|
+
"""
|
|
260
|
+
version = config.project.version
|
|
261
|
+
numeric = pep440_to_file_version(version)
|
|
262
|
+
author = _first_author(config)
|
|
263
|
+
return ResourcePatch(
|
|
264
|
+
icon=icon,
|
|
265
|
+
product_name=config.display_name,
|
|
266
|
+
file_description=config.display_name,
|
|
267
|
+
product_version_string=version,
|
|
268
|
+
legal_copyright=author,
|
|
269
|
+
company_name=author,
|
|
270
|
+
file_version=numeric,
|
|
271
|
+
product_version=numeric,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _copy_app_sources(config: Config, project_root: Path, dest: Path) -> None:
|
|
276
|
+
src = (project_root / config.kivy.app_dir).resolve()
|
|
277
|
+
if not src.is_dir():
|
|
278
|
+
raise WindowsBundleError(
|
|
279
|
+
f"[tool.kivy].app_dir points to {config.kivy.app_dir!r}, which is not "
|
|
280
|
+
f"a directory under {project_root}."
|
|
281
|
+
)
|
|
282
|
+
entry_rel = f"{config.kivy.entry_point.replace('.', '/')}.py"
|
|
283
|
+
entry = src / entry_rel
|
|
284
|
+
if not entry.is_file():
|
|
285
|
+
raise WindowsBundleError(
|
|
286
|
+
f"entry point {entry_rel} not found in "
|
|
287
|
+
f"{config.kivy.app_dir}/ (set [tool.kivy].entry_point)."
|
|
288
|
+
)
|
|
289
|
+
shutil.copytree(src, dest, ignore=_IGNORE)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# Re-exported for the verbs/status so they compute the same path.
|
|
293
|
+
def onedir_path(config: Config, project_root: Path) -> Path:
|
|
294
|
+
return project_root / "build" / "windows" / bundle_dir_name(config)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def launcher_name(config: Config) -> str:
|
|
298
|
+
return launcher_exe_name(config.display_name)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Windows dispatch for the shared ``build`` / ``run`` / ``package`` verbs.
|
|
2
|
+
|
|
3
|
+
Keeps the Windows onedir flow out of the (iOS/Xcode-centric) verb modules: each
|
|
4
|
+
verb resolves the target platform and, when it is Windows, calls in here. Loads
|
|
5
|
+
the Windows config + ``pylock.windows.toml``, runs the drift check, and drives
|
|
6
|
+
the onedir bundler. The onedir folder is both the ``run`` target and — copied to
|
|
7
|
+
``dist/windows`` — the ``package`` output (installers stay external).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from kivyforge.cli._common import ToolchainError, lockfile_path_for
|
|
20
|
+
from kivyforge.config import ConfigError, load_config
|
|
21
|
+
from kivyforge.lock.reader import LockError, is_in_sync
|
|
22
|
+
|
|
23
|
+
from .. import HostCapabilityError, get_platform
|
|
24
|
+
from . import WindowsBundleError
|
|
25
|
+
from .bundle import (
|
|
26
|
+
build_onedir,
|
|
27
|
+
bundle_dir_name,
|
|
28
|
+
launcher_name,
|
|
29
|
+
onedir_path,
|
|
30
|
+
resolve_assembly_arch,
|
|
31
|
+
)
|
|
32
|
+
from .fsswap import discard_reserved, reserve_previous, restore_previous
|
|
33
|
+
from .lock import WindowsLockfile
|
|
34
|
+
from .lock import load as load_windows_lock
|
|
35
|
+
from .signing import select_signer
|
|
36
|
+
|
|
37
|
+
# Never copy VCS metadata or editor droppings into the shipped dist tree. The
|
|
38
|
+
# onedir bundle is assembled clean, so this is defense in depth (and covers a
|
|
39
|
+
# build/ dir a user pointed packaging at directly).
|
|
40
|
+
#
|
|
41
|
+
# Deliberately does NOT exclude __pycache__/*.pyc/*.pyo: those used to be junk
|
|
42
|
+
# patterns before byte-compilation existed, but [tool.kivy.windows
|
|
43
|
+
# .build_settings] now makes .pyc the actual shipped payload -- excluding it
|
|
44
|
+
# here would silently ship an empty app/ once strip_source has deleted the
|
|
45
|
+
# .py, which is exactly what byte_compile/strip_source is supposed to produce.
|
|
46
|
+
_PACKAGE_IGNORE = shutil.ignore_patterns(
|
|
47
|
+
".git",
|
|
48
|
+
".gitignore",
|
|
49
|
+
".gitattributes",
|
|
50
|
+
".svn",
|
|
51
|
+
".hg",
|
|
52
|
+
".DS_Store",
|
|
53
|
+
"Thumbs.db",
|
|
54
|
+
"desktop.ini",
|
|
55
|
+
".idea",
|
|
56
|
+
".vscode",
|
|
57
|
+
"*.swp",
|
|
58
|
+
"*~",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def windows_build(
|
|
63
|
+
project_root: Path,
|
|
64
|
+
*,
|
|
65
|
+
arch: str | None,
|
|
66
|
+
no_verify_lock: bool,
|
|
67
|
+
no_cache: bool,
|
|
68
|
+
) -> Path:
|
|
69
|
+
"""Assemble the Windows onedir bundle; return its path."""
|
|
70
|
+
_require_windows_host()
|
|
71
|
+
config, lock = _load_and_verify(project_root, no_verify_lock)
|
|
72
|
+
bundle = _assemble(
|
|
73
|
+
config, lock, project_root, arch=arch, no_cache=no_cache, release=False
|
|
74
|
+
)
|
|
75
|
+
click.echo(f"Built {bundle.relative_to(project_root)}")
|
|
76
|
+
return bundle
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def windows_package(
|
|
80
|
+
project_root: Path,
|
|
81
|
+
*,
|
|
82
|
+
fmt: str,
|
|
83
|
+
arch: str | None,
|
|
84
|
+
no_verify_lock: bool,
|
|
85
|
+
no_cache: bool,
|
|
86
|
+
) -> Path:
|
|
87
|
+
"""Produce the distributable: the onedir folder copied to ``dist/windows``.
|
|
88
|
+
|
|
89
|
+
``folder`` is the only Windows package format (installers stay external). The
|
|
90
|
+
copied tree is the portable, run-from-folder distributable.
|
|
91
|
+
"""
|
|
92
|
+
_require_windows_host()
|
|
93
|
+
config, lock = _load_and_verify(project_root, no_verify_lock)
|
|
94
|
+
target_arch = _resolve_arch(lock, arch)
|
|
95
|
+
bundle = _assemble(
|
|
96
|
+
config, lock, project_root, arch=target_arch, no_cache=no_cache, release=True
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# The dist folder name matches the build tree: the display_name run through
|
|
100
|
+
# the Windows filename sanitizer (windows-spec), not the raw project.name.
|
|
101
|
+
dest = (
|
|
102
|
+
project_root
|
|
103
|
+
/ "dist"
|
|
104
|
+
/ "windows"
|
|
105
|
+
/ f"{bundle_dir_name(config)}-{config.project.version}-{target_arch}"
|
|
106
|
+
)
|
|
107
|
+
# Reserve any prior package and keep it until *signing* also succeeds, so a
|
|
108
|
+
# signer failure (missing cert, timestamp outage) rolls back to the previous
|
|
109
|
+
# known-good artifact rather than leaving a half-baked unsigned tree in place.
|
|
110
|
+
trash = _stage_dist_copy(bundle, dest)
|
|
111
|
+
try:
|
|
112
|
+
# Signing (when configured) targets the dist copy only, after the resource
|
|
113
|
+
# patch (done at build) and before any external installer. The build tree
|
|
114
|
+
# stays unsigned as the dev-run target.
|
|
115
|
+
signed = _sign_launcher(config, dest)
|
|
116
|
+
except BaseException:
|
|
117
|
+
shutil.rmtree(dest, ignore_errors=True)
|
|
118
|
+
restore_previous(trash, dest)
|
|
119
|
+
raise
|
|
120
|
+
discard_reserved(trash)
|
|
121
|
+
|
|
122
|
+
note = (
|
|
123
|
+
"signed + timestamped"
|
|
124
|
+
if signed
|
|
125
|
+
else "unsigned (configure [tool.kivy.windows.signing] to sign)"
|
|
126
|
+
)
|
|
127
|
+
click.echo(
|
|
128
|
+
f"Packaged {dest.relative_to(project_root)} (onedir folder, {note}).\n"
|
|
129
|
+
f" Run it by double-clicking {launcher_name(config)}, or zip the folder "
|
|
130
|
+
"to distribute. An installer is an external step."
|
|
131
|
+
)
|
|
132
|
+
return dest
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def windows_run(
|
|
136
|
+
project_root: Path,
|
|
137
|
+
*,
|
|
138
|
+
arch: str | None,
|
|
139
|
+
no_build: bool,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Build (unless --no-build) and launch the app ``.exe`` in the foreground."""
|
|
142
|
+
if no_build:
|
|
143
|
+
_require_windows_host()
|
|
144
|
+
config = _load_config(project_root)
|
|
145
|
+
bundle = onedir_path(config, project_root)
|
|
146
|
+
if not bundle.exists():
|
|
147
|
+
raise ToolchainError(
|
|
148
|
+
f"no built bundle at {bundle.relative_to(project_root)}; run "
|
|
149
|
+
"without --no-build first."
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
config = _load_config(project_root)
|
|
153
|
+
bundle = windows_build(
|
|
154
|
+
project_root, arch=arch, no_verify_lock=False, no_cache=False
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
exe = bundle / launcher_name(config)
|
|
158
|
+
click.echo(f"Launching {bundle.name} ...")
|
|
159
|
+
# Foreground: the launcher attaches to this console so the dev sees
|
|
160
|
+
# stdout/stderr + tracebacks (the Phase 3 console handoff).
|
|
161
|
+
proc = subprocess.run([str(exe)])
|
|
162
|
+
if proc.returncode != 0:
|
|
163
|
+
raise ToolchainError(f"{bundle.name} exited with status {proc.returncode}.")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def windows_status(project_root: Path) -> None:
|
|
167
|
+
"""Show app identity, Python version, lock sync, and build state."""
|
|
168
|
+
config = _load_config(project_root)
|
|
169
|
+
windows = config.windows_required
|
|
170
|
+
click.echo(f"App: {config.display_name} ({windows.app_id})")
|
|
171
|
+
click.echo(f"Python: {windows.python_version or '(unset)'}")
|
|
172
|
+
click.echo(f"Lock: {_lock_state(project_root)}")
|
|
173
|
+
click.echo(f"Build: {_build_state(onedir_path(config, project_root))}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _assemble(config, lock, project_root, *, arch, no_cache, release) -> Path:
|
|
177
|
+
try:
|
|
178
|
+
return build_onedir(
|
|
179
|
+
config, lock, project_root, arch=arch, no_cache=no_cache, release=release
|
|
180
|
+
)
|
|
181
|
+
except WindowsBundleError as exc:
|
|
182
|
+
raise ToolchainError(str(exc)) from exc
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_arch(lock: WindowsLockfile, arch: str | None) -> str:
|
|
186
|
+
try:
|
|
187
|
+
return resolve_assembly_arch(lock.archs, arch)
|
|
188
|
+
except WindowsBundleError as exc:
|
|
189
|
+
raise ToolchainError(str(exc)) from exc
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _sign_launcher(config, dest: Path) -> bool:
|
|
193
|
+
"""Sign the launcher in the dist copy when signing is configured.
|
|
194
|
+
|
|
195
|
+
Returns whether the artifact was signed. Translates any signing failure into
|
|
196
|
+
a :class:`ToolchainError` so ``package`` reports it cleanly.
|
|
197
|
+
"""
|
|
198
|
+
signer = select_signer(config.windows_required.signing)
|
|
199
|
+
if not signer.configured:
|
|
200
|
+
return False
|
|
201
|
+
exe = dest / launcher_name(config)
|
|
202
|
+
try:
|
|
203
|
+
signer.sign([exe])
|
|
204
|
+
except WindowsBundleError as exc:
|
|
205
|
+
raise ToolchainError(str(exc)) from exc
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _stage_dist_copy(bundle: Path, dest: Path) -> Path | None:
|
|
210
|
+
"""Copy *bundle* into *dest*, returning the reserved previous tree (or None).
|
|
211
|
+
|
|
212
|
+
Copies **directly into the final location**: like the build step, a freshly
|
|
213
|
+
written tree cannot be reliably renamed on Windows (the antivirus scanner
|
|
214
|
+
holds new files open), so temp-dir + rename is not viable. Any previous dist
|
|
215
|
+
copy is reserved first and restored if the copy fails, so a broken package
|
|
216
|
+
never destroys a working one.
|
|
217
|
+
|
|
218
|
+
The reserved previous tree is **not** discarded here: the caller must call
|
|
219
|
+
:func:`discard_reserved` only after every later step (e.g. signing) succeeds,
|
|
220
|
+
or :func:`restore_previous` to roll back. This keeps the previous known-good
|
|
221
|
+
artifact recoverable across a post-copy failure.
|
|
222
|
+
"""
|
|
223
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
try:
|
|
225
|
+
trash = reserve_previous(dest)
|
|
226
|
+
except WindowsBundleError as exc:
|
|
227
|
+
raise ToolchainError(str(exc)) from exc
|
|
228
|
+
try:
|
|
229
|
+
# copytree needs a non-existent target; the reserve moved any prior away.
|
|
230
|
+
shutil.copytree(bundle, dest, ignore=_PACKAGE_IGNORE)
|
|
231
|
+
except BaseException:
|
|
232
|
+
shutil.rmtree(dest, ignore_errors=True)
|
|
233
|
+
restore_previous(trash, dest)
|
|
234
|
+
raise
|
|
235
|
+
return trash
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _load_and_verify(
|
|
239
|
+
project_root: Path, no_verify_lock: bool
|
|
240
|
+
) -> tuple[object, WindowsLockfile]:
|
|
241
|
+
config = _load_config(project_root)
|
|
242
|
+
lock = _load_lock(project_root)
|
|
243
|
+
pyproject = project_root / "pyproject.toml"
|
|
244
|
+
if not no_verify_lock and not is_in_sync(lock, pyproject.read_text("utf-8")):
|
|
245
|
+
raise ToolchainError(
|
|
246
|
+
f"{lockfile_path_for('windows').name} is out of date with "
|
|
247
|
+
"pyproject.toml.\n"
|
|
248
|
+
" Run `kivyforge lock -p windows` to regenerate it (or pass "
|
|
249
|
+
"--no-verify-lock to build against the stale lock anyway)."
|
|
250
|
+
)
|
|
251
|
+
return config, lock
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _lock_state(project_root: Path) -> str:
|
|
255
|
+
path = lockfile_path_for("windows", project_root)
|
|
256
|
+
if not path.is_file():
|
|
257
|
+
return "missing (run `kivyforge lock -p windows`)"
|
|
258
|
+
try:
|
|
259
|
+
lock = load_windows_lock(path)
|
|
260
|
+
except LockError:
|
|
261
|
+
return "unreadable (run `kivyforge lock -p windows`)"
|
|
262
|
+
pyproject = project_root / "pyproject.toml"
|
|
263
|
+
if is_in_sync(lock, pyproject.read_text("utf-8")):
|
|
264
|
+
return "in sync"
|
|
265
|
+
return "out of date (run `kivyforge lock -p windows`)"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _build_state(bundle: Path) -> str:
|
|
269
|
+
if not bundle.exists():
|
|
270
|
+
return "not built"
|
|
271
|
+
age = time.time() - bundle.stat().st_mtime
|
|
272
|
+
return f"last built {_humanize(age)}"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _humanize(seconds: float) -> str:
|
|
276
|
+
seconds = int(seconds)
|
|
277
|
+
if seconds < 60:
|
|
278
|
+
return "just now"
|
|
279
|
+
if seconds < 3600:
|
|
280
|
+
m = seconds // 60
|
|
281
|
+
return f"{m} minute{'s' if m != 1 else ''} ago"
|
|
282
|
+
if seconds < 86400:
|
|
283
|
+
h = seconds // 3600
|
|
284
|
+
return f"{h} hour{'s' if h != 1 else ''} ago"
|
|
285
|
+
d = seconds // 86400
|
|
286
|
+
return f"{d} day{'s' if d != 1 else ''} ago"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _require_windows_host() -> None:
|
|
290
|
+
try:
|
|
291
|
+
get_platform("windows").check_host_capability()
|
|
292
|
+
except HostCapabilityError as exc:
|
|
293
|
+
raise ToolchainError(str(exc)) from exc
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _load_config(project_root: Path):
|
|
297
|
+
try:
|
|
298
|
+
return load_config(
|
|
299
|
+
project_root / "pyproject.toml", require_ios=False, require_windows=True
|
|
300
|
+
)
|
|
301
|
+
except ConfigError as exc:
|
|
302
|
+
raise ToolchainError(exc.format()) from exc
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _load_lock(project_root: Path) -> WindowsLockfile:
|
|
306
|
+
path = lockfile_path_for("windows", project_root)
|
|
307
|
+
if not path.is_file():
|
|
308
|
+
raise ToolchainError(
|
|
309
|
+
f"no {path.name} found. Run `kivyforge lock -p windows` first."
|
|
310
|
+
)
|
|
311
|
+
try:
|
|
312
|
+
return load_windows_lock(path)
|
|
313
|
+
except LockError as exc:
|
|
314
|
+
raise ToolchainError(str(exc)) from exc
|