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,107 @@
|
|
|
1
|
+
"""Acquire the bundled CPython runtime for the AppDir (linux-spec).
|
|
2
|
+
|
|
3
|
+
Consumes the provider-agnostic runtime pin from ``pylock.linux.toml`` (the
|
|
4
|
+
single ``x86_64`` artifact this phase — one arch per AppImage), fetches +
|
|
5
|
+
verifies its archive, extracts it, and materializes the relocatable CPython tree
|
|
6
|
+
at ``usr/python``. The extracted archive is normalized to its canonical root via
|
|
7
|
+
``normalized_runtime_root`` keyed on the lock's recorded ``provider`` — staging
|
|
8
|
+
never hard-codes a provider's archive internals (spec 07). The PBS linux-gnu
|
|
9
|
+
``install_only`` archive is the same unix-prefix layout as the darwin ones and
|
|
10
|
+
uses ``$ORIGIN``-relative rpaths, so it is relocatable as-shipped — no patchelf
|
|
11
|
+
pass, and (unlike macOS) no ``lipo`` merge, since Linux ships one arch per
|
|
12
|
+
AppImage.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import shutil
|
|
18
|
+
import tarfile
|
|
19
|
+
import tempfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from kivyforge.artifacts.cache import ArtifactCache
|
|
23
|
+
from kivyforge.artifacts.download import DownloadError, fetch_artifact
|
|
24
|
+
from kivyforge.lock.wheelruntime.model import PythonRuntime
|
|
25
|
+
from kivyforge.lock.wheelruntime.runtime import (
|
|
26
|
+
RuntimeProviderError,
|
|
27
|
+
normalized_runtime_root,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from . import AppDirError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def stage_runtime(
|
|
34
|
+
runtime: PythonRuntime,
|
|
35
|
+
arch: str,
|
|
36
|
+
home: Path,
|
|
37
|
+
*,
|
|
38
|
+
project_root: Path,
|
|
39
|
+
cache: ArtifactCache | None = None,
|
|
40
|
+
no_cache: bool = False,
|
|
41
|
+
) -> Path:
|
|
42
|
+
"""Materialize the runtime for *arch* at *home*; return *home*.
|
|
43
|
+
|
|
44
|
+
*home* is the final CPython home directory (whose ``bin/python3`` ``AppRun``
|
|
45
|
+
execs). It is emptied and recreated.
|
|
46
|
+
"""
|
|
47
|
+
cache = cache or ArtifactCache()
|
|
48
|
+
artifact = runtime.artifact_for(arch)
|
|
49
|
+
if artifact is None:
|
|
50
|
+
raise AppDirError(
|
|
51
|
+
f"the lock has no {arch} runtime artifact; re-run "
|
|
52
|
+
f"`kivyforge lock -p linux` (locked archs: "
|
|
53
|
+
f"{', '.join(a.arch for a in runtime.artifacts)})."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
tmp = Path(tempfile.mkdtemp(prefix="kivy-runtime-"))
|
|
57
|
+
try:
|
|
58
|
+
archive = _fetch(artifact, arch, project_root, cache, no_cache)
|
|
59
|
+
extracted = _extract(archive, tmp, runtime.provider)
|
|
60
|
+
if home.exists():
|
|
61
|
+
shutil.rmtree(home)
|
|
62
|
+
home.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
shutil.copytree(extracted, home, symlinks=True)
|
|
64
|
+
return home
|
|
65
|
+
finally:
|
|
66
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _fetch(artifact, arch, project_root, cache, no_cache) -> Path:
|
|
70
|
+
filename = artifact.url.rsplit("/", 1)[-1] or f"python-{arch}.tar.gz"
|
|
71
|
+
try:
|
|
72
|
+
return fetch_artifact(
|
|
73
|
+
name=f"python runtime ({arch})",
|
|
74
|
+
sha256=artifact.sha256,
|
|
75
|
+
filename=filename,
|
|
76
|
+
url=artifact.url,
|
|
77
|
+
project_root=project_root,
|
|
78
|
+
cache=cache,
|
|
79
|
+
no_cache=no_cache,
|
|
80
|
+
)
|
|
81
|
+
except DownloadError as exc:
|
|
82
|
+
raise AppDirError(str(exc)) from exc
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _extract(archive: Path, into: Path, provider: str) -> Path:
|
|
86
|
+
into.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
try:
|
|
88
|
+
with tarfile.open(archive, "r:*") as tf:
|
|
89
|
+
_safe_extractall(tf, into)
|
|
90
|
+
except (tarfile.TarError, OSError) as exc:
|
|
91
|
+
raise AppDirError(f"failed to extract {archive.name}: {exc}") from exc
|
|
92
|
+
try:
|
|
93
|
+
return normalized_runtime_root(provider, into)
|
|
94
|
+
except RuntimeProviderError as exc:
|
|
95
|
+
raise AppDirError(f"{archive.name}: {exc}") from exc
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _safe_extractall(tf: tarfile.TarFile, into: Path) -> None:
|
|
99
|
+
"""Extract, rejecting members that would escape *into* (path traversal)."""
|
|
100
|
+
base = into.resolve()
|
|
101
|
+
for member in tf.getmembers():
|
|
102
|
+
target = (into / member.name).resolve()
|
|
103
|
+
if not (target == base or base in target.parents):
|
|
104
|
+
raise AppDirError(f"unsafe path in archive: {member.name!r}")
|
|
105
|
+
# Paths are validated above; fully_trusted preserves the runtime's
|
|
106
|
+
# permissions and symlinks (the restrictive filters would rewrite them).
|
|
107
|
+
tf.extractall(into, filter="fully_trusted") # noqa: S202 — members validated just above
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Install the locked wheels into the AppDir's ``usr/lib`` (site-packages).
|
|
2
|
+
|
|
3
|
+
Wheels are fetched + SHA-256-verified from ``pylock.linux.toml`` (URL or
|
|
4
|
+
vendored path) and unpacked directly — no pip, so the bundler is independent of
|
|
5
|
+
the host interpreter. Linux ships one arch per AppImage, so per package a single
|
|
6
|
+
wheel satisfies coverage: a pure-Python (``py3-none-any``) wheel or a manylinux
|
|
7
|
+
(or vendored ``linux_x86_64``) wheel matching the assembly arch. There is no
|
|
8
|
+
``lipo``-style merge as on macOS.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import shutil
|
|
14
|
+
import zipfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from kivyforge.artifacts.cache import ArtifactCache
|
|
18
|
+
from kivyforge.artifacts.download import DownloadError, fetch_artifact
|
|
19
|
+
from kivyforge.lock.model import LockedPackage, LockedWheel
|
|
20
|
+
|
|
21
|
+
from . import AppDirError
|
|
22
|
+
from .lock import linux_wheel_arch
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def stage_wheels(
|
|
26
|
+
packages: tuple[LockedPackage, ...],
|
|
27
|
+
arch: str,
|
|
28
|
+
lib_dir: Path,
|
|
29
|
+
*,
|
|
30
|
+
project_root: Path,
|
|
31
|
+
cache: ArtifactCache | None = None,
|
|
32
|
+
no_cache: bool = False,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Unpack every package's wheel for *arch* into *lib_dir*."""
|
|
35
|
+
cache = cache or ArtifactCache()
|
|
36
|
+
if lib_dir.exists():
|
|
37
|
+
shutil.rmtree(lib_dir)
|
|
38
|
+
lib_dir.mkdir(parents=True)
|
|
39
|
+
|
|
40
|
+
for pkg in packages:
|
|
41
|
+
wheel = _select_wheel(pkg, arch)
|
|
42
|
+
_unpack(_fetch(wheel, project_root, cache, no_cache), lib_dir)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _select_wheel(pkg: LockedPackage, arch: str) -> LockedWheel:
|
|
46
|
+
"""Pick the wheel to unpack: pure-Python, else the one covering *arch*."""
|
|
47
|
+
for wheel in pkg.wheels:
|
|
48
|
+
if wheel.is_pure_python:
|
|
49
|
+
return wheel
|
|
50
|
+
for wheel in pkg.wheels:
|
|
51
|
+
if any(linux_wheel_arch(sub) == arch for sub in wheel.platform_tag.split(".")):
|
|
52
|
+
return wheel
|
|
53
|
+
raise AppDirError(
|
|
54
|
+
f"{pkg.name} {pkg.version} has no wheel for arch {arch} in the lock.\n"
|
|
55
|
+
" Re-run `kivyforge lock -p linux`."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _fetch(wheel: LockedWheel, project_root, cache, no_cache) -> Path:
|
|
60
|
+
try:
|
|
61
|
+
return fetch_artifact(
|
|
62
|
+
name=wheel.name,
|
|
63
|
+
sha256=wheel.sha256,
|
|
64
|
+
filename=wheel.name,
|
|
65
|
+
url=wheel.url,
|
|
66
|
+
path=wheel.path,
|
|
67
|
+
project_root=project_root,
|
|
68
|
+
cache=cache,
|
|
69
|
+
no_cache=no_cache,
|
|
70
|
+
)
|
|
71
|
+
except DownloadError as exc:
|
|
72
|
+
raise AppDirError(str(exc)) from exc
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _unpack(wheel: Path, target: Path) -> None:
|
|
76
|
+
"""Extract a wheel into *target*, normalizing ``.data/{purelib,platlib}``."""
|
|
77
|
+
with zipfile.ZipFile(wheel) as zf:
|
|
78
|
+
_safe_extract(zf, target)
|
|
79
|
+
for data_dir in list(target.glob("*.data")):
|
|
80
|
+
for sub in ("purelib", "platlib"):
|
|
81
|
+
src = data_dir / sub
|
|
82
|
+
if src.is_dir():
|
|
83
|
+
_merge_tree(src, target)
|
|
84
|
+
shutil.rmtree(data_dir, ignore_errors=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _merge_tree(src: Path, dest: Path) -> None:
|
|
88
|
+
for item in src.iterdir():
|
|
89
|
+
target = dest / item.name
|
|
90
|
+
if item.is_dir():
|
|
91
|
+
target.mkdir(exist_ok=True)
|
|
92
|
+
_merge_tree(item, target)
|
|
93
|
+
else:
|
|
94
|
+
shutil.copy2(item, target)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _safe_extract(zf: zipfile.ZipFile, target: Path) -> None:
|
|
98
|
+
base = target.resolve()
|
|
99
|
+
for member in zf.namelist():
|
|
100
|
+
resolved = (target / member).resolve()
|
|
101
|
+
if not (resolved == base or base in resolved.parents):
|
|
102
|
+
raise AppDirError(f"unsafe path in wheel: {member!r}")
|
|
103
|
+
zf.extractall(target) # noqa: S202 — members validated just above
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""macOS platform backend.
|
|
2
|
+
|
|
3
|
+
Produces a self-contained ``.app`` bundle (macos-spec). macOS is the host
|
|
4
|
+
default on Darwin, so a Mac with ``[tool.kivy.macos]`` configured resolves to
|
|
5
|
+
this backend without ``-p``. It requires a macOS host (``codesign``/``lipo``
|
|
6
|
+
from the Xcode command-line tools); the full Xcode IDE is not needed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import platform as _platform
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from ..base import HostCapabilityError, Platform
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from kivyforge.doctor.result import CheckResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AppBundleError(Exception):
|
|
22
|
+
"""A macOS ``.app`` bundling failure surfaced with an actionable message."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MacosPlatform(Platform):
|
|
26
|
+
name = "macos"
|
|
27
|
+
# macOS is a desktop target built on its own OS, so Darwin maps to it as the
|
|
28
|
+
# host default (resolution-chain step 3).
|
|
29
|
+
host_system = "Darwin"
|
|
30
|
+
# The distributable is the .app bundle; .dmg/installer is external.
|
|
31
|
+
package_formats = ("app",)
|
|
32
|
+
|
|
33
|
+
def check_host_capability(self, *, host_system: str | None = None) -> None:
|
|
34
|
+
host = host_system if host_system is not None else _platform.system()
|
|
35
|
+
if host != "Darwin":
|
|
36
|
+
raise HostCapabilityError(
|
|
37
|
+
"building for macOS requires a macOS host; "
|
|
38
|
+
f"this host is {host!r}.\n"
|
|
39
|
+
" The .app bundler signs Mach-O binaries with codesign and "
|
|
40
|
+
"merges per-arch runtimes with lipo — both are macOS-only.\n"
|
|
41
|
+
" Run `kivyforge doctor -p macos` on a Mac to check the setup."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def build(
|
|
45
|
+
self,
|
|
46
|
+
project_root: Path,
|
|
47
|
+
*,
|
|
48
|
+
target: str | None,
|
|
49
|
+
arch: str | None,
|
|
50
|
+
no_verify_lock: bool,
|
|
51
|
+
no_cache: bool,
|
|
52
|
+
team_id: str | None,
|
|
53
|
+
signing_identity: str | None,
|
|
54
|
+
export_method: str,
|
|
55
|
+
debug: bool = False,
|
|
56
|
+
fmt: str | None = None,
|
|
57
|
+
abi: str | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self.reject_ios_only_target(target)
|
|
60
|
+
from .cli import macos_build
|
|
61
|
+
|
|
62
|
+
macos_build(
|
|
63
|
+
project_root,
|
|
64
|
+
arch=arch,
|
|
65
|
+
no_verify_lock=no_verify_lock,
|
|
66
|
+
no_cache=no_cache,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def run(
|
|
70
|
+
self,
|
|
71
|
+
project_root: Path,
|
|
72
|
+
*,
|
|
73
|
+
target: str,
|
|
74
|
+
arch: str | None,
|
|
75
|
+
destination: str | None,
|
|
76
|
+
no_build: bool,
|
|
77
|
+
) -> None:
|
|
78
|
+
from .cli import macos_run
|
|
79
|
+
|
|
80
|
+
macos_run(project_root, arch=arch, no_build=no_build)
|
|
81
|
+
|
|
82
|
+
def package(
|
|
83
|
+
self,
|
|
84
|
+
project_root: Path,
|
|
85
|
+
*,
|
|
86
|
+
fmt: str,
|
|
87
|
+
arch: str | None,
|
|
88
|
+
team_id: str | None,
|
|
89
|
+
signing_identity: str | None,
|
|
90
|
+
export_method: str,
|
|
91
|
+
notarize: bool | None,
|
|
92
|
+
notary_profile: str | None,
|
|
93
|
+
no_verify_lock: bool,
|
|
94
|
+
no_cache: bool,
|
|
95
|
+
abi: str | None = None,
|
|
96
|
+
keystore: str | None = None,
|
|
97
|
+
key_alias: str | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
from .cli import macos_package
|
|
100
|
+
|
|
101
|
+
macos_package(
|
|
102
|
+
project_root,
|
|
103
|
+
arch=arch,
|
|
104
|
+
no_verify_lock=no_verify_lock,
|
|
105
|
+
no_cache=no_cache,
|
|
106
|
+
signing_identity=signing_identity,
|
|
107
|
+
notarize=notarize,
|
|
108
|
+
notary_profile=notary_profile,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def status(self, project_root: Path) -> None:
|
|
112
|
+
from .cli import macos_status
|
|
113
|
+
|
|
114
|
+
macos_status(project_root)
|
|
115
|
+
|
|
116
|
+
def doctor(
|
|
117
|
+
self, cwd: Path, *, kivyforge_version: str, offline: bool
|
|
118
|
+
) -> list[CheckResult]:
|
|
119
|
+
from .doctor import macos_doctor
|
|
120
|
+
|
|
121
|
+
return macos_doctor(cwd, kivyforge_version=kivyforge_version, offline=offline)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Assemble (and optionally sign) the macOS ``.app`` bundle (macos-spec).
|
|
2
|
+
|
|
3
|
+
Orchestrates the pieces: resolve the assembly arch from the lock + ``--arch``
|
|
4
|
+
(macOS is arm64-only, so this is always a single arch), stage the runtime +
|
|
5
|
+
wheels, copy the app sources, render the icon + Info.plist + launcher, and
|
|
6
|
+
ad-hoc sign. Produces the layout documented in macos-spec:
|
|
7
|
+
|
|
8
|
+
<Name>.app/Contents/{Info.plist, MacOS/<exe>,
|
|
9
|
+
Resources/{app,lib,python,<exe>.icns}}
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import platform
|
|
15
|
+
import plistlib
|
|
16
|
+
import shutil
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from kivyforge.artifacts.cache import ArtifactCache
|
|
23
|
+
from kivyforge.bundle.pycompile import PycompileError, byte_compile, select_compiler
|
|
24
|
+
from kivyforge.config.model import Config
|
|
25
|
+
|
|
26
|
+
from . import AppBundleError
|
|
27
|
+
from .icns import generate_icns
|
|
28
|
+
from .launcher import build_launcher
|
|
29
|
+
from .lock import MacosLockfile
|
|
30
|
+
from .native_stage import stage_native_binaries
|
|
31
|
+
from .plist import build_info_plist
|
|
32
|
+
from .runtime_stage import stage_runtime
|
|
33
|
+
from .signing import sign_bundle_adhoc
|
|
34
|
+
from .wheels_stage import stage_wheels
|
|
35
|
+
|
|
36
|
+
_IGNORE = shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_assembly_arch(locked: tuple[str, ...], arch: str | None) -> str:
|
|
40
|
+
"""The single arch to assemble. macOS ships arm64 only."""
|
|
41
|
+
if not locked:
|
|
42
|
+
raise AppBundleError(
|
|
43
|
+
"the lock covers no architectures; re-run `kivyforge lock -p macos`."
|
|
44
|
+
)
|
|
45
|
+
if arch is None:
|
|
46
|
+
return locked[0]
|
|
47
|
+
if arch not in locked:
|
|
48
|
+
raise AppBundleError(
|
|
49
|
+
f"--arch {arch} is not in the lock (covers {', '.join(locked)}).\n"
|
|
50
|
+
f" macOS builds are arm64-only; pick a locked arch."
|
|
51
|
+
)
|
|
52
|
+
return arch
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _setting_applies(value: bool | str, *, release: bool) -> bool:
|
|
56
|
+
"""A ``build_settings`` tri-state resolved for the build being produced."""
|
|
57
|
+
if isinstance(value, bool):
|
|
58
|
+
return value
|
|
59
|
+
return release # DESKTOP_RELEASE_ONLY
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _resolve_byte_compile(
|
|
63
|
+
config: Config,
|
|
64
|
+
*,
|
|
65
|
+
staged_interpreter: Path,
|
|
66
|
+
target_arch: str,
|
|
67
|
+
python_version: str,
|
|
68
|
+
release: bool,
|
|
69
|
+
) -> tuple[tuple[str, ...] | None, bool]:
|
|
70
|
+
"""Decide whether to byte-compile, and with which interpreter.
|
|
71
|
+
|
|
72
|
+
Returns ``(compiler_argv_or_None, strip_source)``; ``None`` means do not
|
|
73
|
+
byte-compile. ``byte_compile = true`` is read as "I insist", so a missing
|
|
74
|
+
interpreter is an error; the default ``"release"`` degrades to shipping
|
|
75
|
+
source with a warning rather than breaking a build the user never
|
|
76
|
+
configured.
|
|
77
|
+
"""
|
|
78
|
+
settings = config.macos_required.build_settings
|
|
79
|
+
if not _setting_applies(settings.byte_compile, release=release):
|
|
80
|
+
return None, False
|
|
81
|
+
native = platform.machine().lower() == target_arch
|
|
82
|
+
compiler = select_compiler(
|
|
83
|
+
staged_interpreter=staged_interpreter,
|
|
84
|
+
native=native,
|
|
85
|
+
python_version=python_version,
|
|
86
|
+
)
|
|
87
|
+
if compiler is None:
|
|
88
|
+
message = (
|
|
89
|
+
f"this project ships CPython {python_version}, and no CPython of "
|
|
90
|
+
"that minor could be found to byte-compile with (a .pyc is only "
|
|
91
|
+
"loadable by the exact CPython minor that wrote it)"
|
|
92
|
+
)
|
|
93
|
+
if settings.byte_compile is True:
|
|
94
|
+
raise AppBundleError(
|
|
95
|
+
"[tool.kivy.macos.build_settings].byte_compile = true but "
|
|
96
|
+
f"{message}.\n"
|
|
97
|
+
" Install a matching CPython, or set byte_compile = false."
|
|
98
|
+
)
|
|
99
|
+
click.echo(f"[stage] not byte-compiling: {message}.")
|
|
100
|
+
return None, False
|
|
101
|
+
strip = _setting_applies(settings.strip_source, release=release)
|
|
102
|
+
return compiler, strip
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def build_app_bundle(
|
|
106
|
+
config: Config,
|
|
107
|
+
lock: MacosLockfile,
|
|
108
|
+
project_root: Path,
|
|
109
|
+
*,
|
|
110
|
+
arch: str | None = None,
|
|
111
|
+
staging_dir: Path | None = None,
|
|
112
|
+
sign: bool = True,
|
|
113
|
+
no_cache: bool = False,
|
|
114
|
+
cache: ArtifactCache | None = None,
|
|
115
|
+
release: bool = False,
|
|
116
|
+
echo=click.echo,
|
|
117
|
+
) -> Path:
|
|
118
|
+
"""Build the ``.app`` and return its path."""
|
|
119
|
+
target_arch = resolve_assembly_arch(lock.archs, arch)
|
|
120
|
+
cache = cache or ArtifactCache()
|
|
121
|
+
staging_dir = staging_dir or (project_root / "build" / "macos")
|
|
122
|
+
exe = config.app_slug
|
|
123
|
+
app = staging_dir / f"{config.display_name}.app"
|
|
124
|
+
|
|
125
|
+
# Assemble into a temp .app and swap it in only on success, so a failure
|
|
126
|
+
# mid-assembly (e.g. a transient runtime/wheel fetch error, or a signing
|
|
127
|
+
# failure) leaves the previous, working .app untouched instead of a
|
|
128
|
+
# half-written one. Mirrors the Linux AppDir builder's write-then-swap.
|
|
129
|
+
staging_dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
work = Path(tempfile.mkdtemp(dir=staging_dir, prefix=f".{app.name}.tmp-"))
|
|
131
|
+
try:
|
|
132
|
+
contents = work / "Contents"
|
|
133
|
+
(contents / "MacOS").mkdir(parents=True)
|
|
134
|
+
resources = contents / "Resources"
|
|
135
|
+
resources.mkdir()
|
|
136
|
+
|
|
137
|
+
echo(
|
|
138
|
+
f"Staging CPython {lock.python_runtime.version} runtime ({target_arch}) ..."
|
|
139
|
+
)
|
|
140
|
+
stage_runtime(
|
|
141
|
+
lock.python_runtime,
|
|
142
|
+
target_arch,
|
|
143
|
+
resources / "python",
|
|
144
|
+
project_root=project_root,
|
|
145
|
+
cache=cache,
|
|
146
|
+
no_cache=no_cache,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
echo(f"Installing {len(lock.packages)} locked packages ...")
|
|
150
|
+
stage_wheels(
|
|
151
|
+
lock.packages,
|
|
152
|
+
target_arch,
|
|
153
|
+
resources / "lib",
|
|
154
|
+
project_root=project_root,
|
|
155
|
+
cache=cache,
|
|
156
|
+
no_cache=no_cache,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if lock.native_binaries:
|
|
160
|
+
echo(f"Staging {len(lock.native_binaries)} native binaries ...")
|
|
161
|
+
stage_native_binaries(
|
|
162
|
+
lock,
|
|
163
|
+
resources,
|
|
164
|
+
project_root=project_root,
|
|
165
|
+
cache=cache,
|
|
166
|
+
no_cache=no_cache,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
_copy_app_sources(config, project_root, resources / "app")
|
|
170
|
+
icon_file = _stage_icon(config, project_root, resources, exe)
|
|
171
|
+
|
|
172
|
+
build_launcher(
|
|
173
|
+
contents / "MacOS" / exe,
|
|
174
|
+
entry_point=config.kivy.entry_point,
|
|
175
|
+
arch=target_arch,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
plist = build_info_plist(config, executable=exe, icon_file=icon_file)
|
|
179
|
+
with (contents / "Info.plist").open("wb") as fh:
|
|
180
|
+
plistlib.dump(plist, fh)
|
|
181
|
+
|
|
182
|
+
# Byte-compile before signing: codesign seals every file it signs, so
|
|
183
|
+
# mutating the payload afterward (deleting .py, writing .pyc) would
|
|
184
|
+
# invalidate the very signature this function is about to produce.
|
|
185
|
+
compiler, strip_source = _resolve_byte_compile(
|
|
186
|
+
config,
|
|
187
|
+
staged_interpreter=resources / "python" / "bin" / "python3",
|
|
188
|
+
target_arch=target_arch,
|
|
189
|
+
python_version=lock.python_runtime.version,
|
|
190
|
+
release=release,
|
|
191
|
+
)
|
|
192
|
+
if compiler is not None:
|
|
193
|
+
with_what = " ".join(compiler) if compiler else "this interpreter"
|
|
194
|
+
echo(
|
|
195
|
+
f"[stage] byte-compiling the Python payload with {with_what}"
|
|
196
|
+
+ (" (.pyc only)" if strip_source else "")
|
|
197
|
+
)
|
|
198
|
+
try:
|
|
199
|
+
byte_compile(
|
|
200
|
+
[resources / "app", resources / "lib"],
|
|
201
|
+
compiler=compiler,
|
|
202
|
+
strip_source=strip_source,
|
|
203
|
+
stripdir=work,
|
|
204
|
+
)
|
|
205
|
+
except PycompileError as exc:
|
|
206
|
+
raise AppBundleError(
|
|
207
|
+
f"{exc}\n Fix it, or set "
|
|
208
|
+
"[tool.kivy.macos.build_settings].byte_compile = false."
|
|
209
|
+
) from exc
|
|
210
|
+
|
|
211
|
+
# Sign the temp bundle before the swap: codesign embeds signatures in the
|
|
212
|
+
# Mach-O files + Contents/_CodeSignature, so an atomic rename preserves
|
|
213
|
+
# them, and a signing failure never displaces the previous good .app.
|
|
214
|
+
if sign:
|
|
215
|
+
echo("Ad-hoc signing the bundle ...")
|
|
216
|
+
count = sign_bundle_adhoc(work)
|
|
217
|
+
echo(f" signed {count} Mach-O binaries + the bundle")
|
|
218
|
+
except BaseException:
|
|
219
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
220
|
+
raise
|
|
221
|
+
|
|
222
|
+
# Swap the freshly built tree in for the previous one. The rename is atomic
|
|
223
|
+
# within staging_dir; the brief rmtree→rename window only exists after a
|
|
224
|
+
# fully successful build.
|
|
225
|
+
if app.exists():
|
|
226
|
+
shutil.rmtree(app)
|
|
227
|
+
work.replace(app)
|
|
228
|
+
return app
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _copy_app_sources(config: Config, project_root: Path, dest: Path) -> None:
|
|
232
|
+
src = (project_root / config.kivy.app_dir).resolve()
|
|
233
|
+
if not src.is_dir():
|
|
234
|
+
raise AppBundleError(
|
|
235
|
+
f"[tool.kivy].app_dir points to {config.kivy.app_dir!r}, which is not "
|
|
236
|
+
f"a directory under {project_root}."
|
|
237
|
+
)
|
|
238
|
+
entry = src / f"{config.kivy.entry_point}.py"
|
|
239
|
+
if not entry.is_file():
|
|
240
|
+
raise AppBundleError(
|
|
241
|
+
f"entry point {config.kivy.entry_point}.py not found in "
|
|
242
|
+
f"{config.kivy.app_dir}/ (set [tool.kivy].entry_point)."
|
|
243
|
+
)
|
|
244
|
+
shutil.copytree(src, dest, ignore=_IGNORE)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _stage_icon(
|
|
248
|
+
config: Config, project_root: Path, resources: Path, exe: str
|
|
249
|
+
) -> str | None:
|
|
250
|
+
source = config.macos_required.icons.source
|
|
251
|
+
if not source:
|
|
252
|
+
return None
|
|
253
|
+
icns = f"{exe}.icns"
|
|
254
|
+
generate_icns((project_root / source).resolve(), resources / icns)
|
|
255
|
+
return icns
|