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,438 @@
|
|
|
1
|
+
"""Linux-specific doctor checks (linux-spec).
|
|
2
|
+
|
|
3
|
+
Reuses the platform-neutral checks (kivyforge version, app source dir) from
|
|
4
|
+
``checks`` and adds the Linux environment (host libc, GL libraries, display
|
|
5
|
+
session) and project checks (glibc floor, arch coverage, icon, generated
|
|
6
|
+
``.desktop`` validity, find_links, reachable hosts) over the
|
|
7
|
+
``pylock.linux.toml`` model. The reachable-hosts check also covers the pinned
|
|
8
|
+
appimagetool / type2-runtime asset hosts, since packaging fetches them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
from kivyforge.config import ConfigError, load_config
|
|
18
|
+
from kivyforge.config.icons import APP_ICON_SIZE, icon_source_problem
|
|
19
|
+
from kivyforge.config.model import Config
|
|
20
|
+
from kivyforge.doctor import checks_common as C
|
|
21
|
+
from kivyforge.doctor.checks_common import _ver_tuple
|
|
22
|
+
from kivyforge.doctor.probe import Probe, RealProbe
|
|
23
|
+
from kivyforge.doctor.result import CheckResult, Status
|
|
24
|
+
from kivyforge.lock.find_links import find_links_doctor_detail
|
|
25
|
+
from kivyforge.lock.reader import LockError
|
|
26
|
+
|
|
27
|
+
from . import AppDirError
|
|
28
|
+
from .appimage import appimagetool_asset, type2_runtime_asset
|
|
29
|
+
from .desktop import render_desktop_entry
|
|
30
|
+
from .elftools import ARCH_ELF, describe, elf_machine, is_elf
|
|
31
|
+
from .lock import (
|
|
32
|
+
LinuxLockfile,
|
|
33
|
+
effective_glibc_floor,
|
|
34
|
+
linux_wheel_coverage,
|
|
35
|
+
load,
|
|
36
|
+
)
|
|
37
|
+
from .lock.runtime import DEFAULT_GLIBC_FLOOR
|
|
38
|
+
|
|
39
|
+
_GL_LIBS = ("libGL.so.1", "libEGL.so.1")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---- environment checks -------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_linux_host(probe: Probe) -> CheckResult:
|
|
46
|
+
host = probe.host_system()
|
|
47
|
+
if host != "Linux":
|
|
48
|
+
return CheckResult(
|
|
49
|
+
"Host is Linux",
|
|
50
|
+
Status.FAIL,
|
|
51
|
+
f"host is {host!r}",
|
|
52
|
+
hint="the AppImage/AppDir backend bundles a gnu/glibc "
|
|
53
|
+
"python-build-standalone runtime and requires a Linux host.",
|
|
54
|
+
)
|
|
55
|
+
if probe.linux_libc() == "musl":
|
|
56
|
+
return CheckResult(
|
|
57
|
+
"Host is Linux",
|
|
58
|
+
Status.WARN,
|
|
59
|
+
"musl libc host",
|
|
60
|
+
hint="the bundled python-build-standalone runtime is a gnu/glibc "
|
|
61
|
+
"build and will not run on musl (Alpine); build on a glibc host.",
|
|
62
|
+
)
|
|
63
|
+
return CheckResult("Host is Linux", Status.PASS, "Linux (glibc)")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def check_gl_libraries(probe: Probe) -> CheckResult:
|
|
67
|
+
libs = probe.shared_libraries()
|
|
68
|
+
missing = [lib for lib in _GL_LIBS if lib not in libs]
|
|
69
|
+
if missing:
|
|
70
|
+
return CheckResult(
|
|
71
|
+
"GL libraries",
|
|
72
|
+
Status.WARN,
|
|
73
|
+
f"{', '.join(missing)} not found by ldconfig",
|
|
74
|
+
hint="libGL/libEGL come from the host, never the bundle. Install "
|
|
75
|
+
"your distro's OpenGL runtime (Debian/Ubuntu: libgl1 libegl1; "
|
|
76
|
+
"Fedora: mesa-libGL mesa-libEGL).",
|
|
77
|
+
)
|
|
78
|
+
return CheckResult("GL libraries", Status.PASS, ", ".join(_GL_LIBS))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def check_display_session(probe: Probe) -> CheckResult:
|
|
82
|
+
sessions = probe.display_session()
|
|
83
|
+
if not sessions:
|
|
84
|
+
return CheckResult(
|
|
85
|
+
"Display session",
|
|
86
|
+
Status.WARN,
|
|
87
|
+
"no DISPLAY or WAYLAND_DISPLAY",
|
|
88
|
+
hint="the app needs an X11 or Wayland session to open a window; "
|
|
89
|
+
"for headless CI run it under `xvfb-run` with software GL.",
|
|
90
|
+
)
|
|
91
|
+
return CheckResult("Display session", Status.PASS, ", ".join(sorted(sessions)))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---- project checks -----------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def check_linux_glibc_floor(config: Config, lock: LinuxLockfile | None) -> CheckResult:
|
|
98
|
+
declared = config.linux_required.glibc_floor
|
|
99
|
+
runtime_floor = (lock.python_runtime.floor if lock else None) or DEFAULT_GLIBC_FLOOR
|
|
100
|
+
if declared and _ver_tuple(declared) < _ver_tuple(runtime_floor):
|
|
101
|
+
return CheckResult(
|
|
102
|
+
"glibc floor",
|
|
103
|
+
Status.FAIL,
|
|
104
|
+
f"glibc_floor {declared} < runtime floor {runtime_floor}",
|
|
105
|
+
hint=f"raise [tool.kivy.linux].glibc_floor to >= {runtime_floor}.",
|
|
106
|
+
)
|
|
107
|
+
if lock is None:
|
|
108
|
+
return CheckResult("glibc floor", Status.SKIP, "no pylock.linux.toml")
|
|
109
|
+
effective = effective_glibc_floor(lock)
|
|
110
|
+
return CheckResult(
|
|
111
|
+
"glibc floor", Status.PASS, f"effective host floor glibc {effective}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_linux_arch_coverage(
|
|
116
|
+
config: Config, lock: LinuxLockfile | None
|
|
117
|
+
) -> CheckResult:
|
|
118
|
+
archs = config.linux_required.archs
|
|
119
|
+
if lock is None:
|
|
120
|
+
return CheckResult("Architecture coverage", Status.SKIP, "no pylock.linux.toml")
|
|
121
|
+
|
|
122
|
+
runtime_archs = {a.arch for a in lock.python_runtime.artifacts}
|
|
123
|
+
missing_runtime = [a for a in archs if a not in runtime_archs]
|
|
124
|
+
if missing_runtime:
|
|
125
|
+
return CheckResult(
|
|
126
|
+
"Architecture coverage",
|
|
127
|
+
Status.FAIL,
|
|
128
|
+
f"runtime missing arch(es): {', '.join(missing_runtime)}",
|
|
129
|
+
hint="re-run `kivyforge lock -p linux` after setting archs.",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
plain_linux_warnings: list[str] = []
|
|
133
|
+
for pkg in lock.packages:
|
|
134
|
+
if any(w.is_pure_python for w in pkg.wheels):
|
|
135
|
+
continue
|
|
136
|
+
# Source-aware coverage: a plain linux_* wheel counts only when vendored
|
|
137
|
+
# via find_links, and yields the mandated no-glibc-promise warning.
|
|
138
|
+
covered: set[str] = set()
|
|
139
|
+
for wheel in pkg.wheels:
|
|
140
|
+
wheel_archs, warning = linux_wheel_coverage(wheel, archs)
|
|
141
|
+
covered.update(wheel_archs)
|
|
142
|
+
if warning:
|
|
143
|
+
plain_linux_warnings.append(f"{pkg.name}: {warning}")
|
|
144
|
+
missing = [a for a in archs if a not in covered]
|
|
145
|
+
if missing:
|
|
146
|
+
return CheckResult(
|
|
147
|
+
"Architecture coverage",
|
|
148
|
+
Status.FAIL,
|
|
149
|
+
f"{pkg.name} missing manylinux wheel(s) for: {', '.join(missing)}",
|
|
150
|
+
hint="a compiled dep needs a manylinux wheel per arch; raise "
|
|
151
|
+
"[tool.kivy.linux].glibc_floor or supply it via "
|
|
152
|
+
"extra_index_urls/find_links, then re-lock.",
|
|
153
|
+
)
|
|
154
|
+
if plain_linux_warnings:
|
|
155
|
+
return CheckResult(
|
|
156
|
+
"Architecture coverage",
|
|
157
|
+
Status.WARN,
|
|
158
|
+
f"covers {', '.join(archs)} but relies on vendored plain linux_* wheel(s)",
|
|
159
|
+
hint="; ".join(plain_linux_warnings),
|
|
160
|
+
)
|
|
161
|
+
return CheckResult(
|
|
162
|
+
"Architecture coverage", Status.PASS, f"all deps cover {', '.join(archs)}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def check_linux_app_icon(config: Config, project_root: Path) -> CheckResult:
|
|
167
|
+
source = config.linux_required.icons.source
|
|
168
|
+
if not source:
|
|
169
|
+
return CheckResult("App icon", Status.SKIP, "not configured")
|
|
170
|
+
problem = icon_source_problem((project_root / source).resolve())
|
|
171
|
+
if problem:
|
|
172
|
+
return CheckResult("App icon", Status.FAIL, f"{source} invalid", hint=problem)
|
|
173
|
+
return CheckResult("App icon", Status.PASS, f"{APP_ICON_SIZE}x{APP_ICON_SIZE} PNG")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def check_linux_desktop_entry(probe: Probe, config: Config) -> CheckResult:
|
|
177
|
+
if not probe.has_desktop_file_validate():
|
|
178
|
+
return CheckResult(
|
|
179
|
+
"Desktop entry valid",
|
|
180
|
+
Status.SKIP,
|
|
181
|
+
"desktop-file-validate not installed",
|
|
182
|
+
)
|
|
183
|
+
app_id = config.linux_required.app_id
|
|
184
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
185
|
+
path = Path(tmp) / f"{app_id}.desktop"
|
|
186
|
+
path.write_text(render_desktop_entry(config), encoding="utf-8")
|
|
187
|
+
errors = probe.desktop_file_errors(path)
|
|
188
|
+
if errors:
|
|
189
|
+
return CheckResult(
|
|
190
|
+
"Desktop entry valid",
|
|
191
|
+
Status.FAIL,
|
|
192
|
+
"generated .desktop failed validation",
|
|
193
|
+
hint=errors,
|
|
194
|
+
)
|
|
195
|
+
return CheckResult("Desktop entry valid", Status.PASS, f"{app_id}.desktop")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def check_linux_find_links(config: Config, project_root: Path) -> CheckResult:
|
|
199
|
+
entries = config.linux_required.find_links
|
|
200
|
+
if not entries:
|
|
201
|
+
return CheckResult("find_links directories", Status.SKIP, "not configured")
|
|
202
|
+
root = project_root.resolve()
|
|
203
|
+
problems: list[tuple[str, str | None]] = []
|
|
204
|
+
warnings: list[tuple[str, str | None]] = []
|
|
205
|
+
ok: list[str] = []
|
|
206
|
+
for entry in entries:
|
|
207
|
+
path = (root / entry).resolve()
|
|
208
|
+
detail, hint = find_links_doctor_detail(root, entry, path, platform="linux")
|
|
209
|
+
if not path.is_dir():
|
|
210
|
+
problems.append((detail, hint))
|
|
211
|
+
elif not any(path.glob("*.whl")):
|
|
212
|
+
warnings.append((detail, hint))
|
|
213
|
+
else:
|
|
214
|
+
ok.append(detail)
|
|
215
|
+
if problems:
|
|
216
|
+
return CheckResult(
|
|
217
|
+
"find_links directories",
|
|
218
|
+
Status.FAIL,
|
|
219
|
+
"; ".join(d for d, _ in problems),
|
|
220
|
+
hint=next((h for _, h in problems if h), ""),
|
|
221
|
+
)
|
|
222
|
+
if warnings:
|
|
223
|
+
return CheckResult(
|
|
224
|
+
"find_links directories",
|
|
225
|
+
Status.WARN,
|
|
226
|
+
"; ".join(d for d, _ in warnings),
|
|
227
|
+
hint=next((h for _, h in warnings if h), ""),
|
|
228
|
+
)
|
|
229
|
+
return CheckResult("find_links directories", Status.PASS, "; ".join(ok))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def check_linux_native_binaries(config: Config, project_root: Path) -> CheckResult:
|
|
233
|
+
"""Vendored native-binary sources exist and (once built) match the arch.
|
|
234
|
+
|
|
235
|
+
SKIPs when no ``[tool.kivy.linux.native.binaries]`` are declared. Vendored
|
|
236
|
+
(repo-relative) sources must be present on disk; URL sources are re-checked
|
|
237
|
+
for reachability by ``check_linux_hosts_reachable``. No two single-file
|
|
238
|
+
(non-archive) sources may share a staged basename (they would overwrite each
|
|
239
|
+
other in ``usr/bin``; archive-member collisions are caught at build time).
|
|
240
|
+
When the AppDir has been built, every ELF staged under ``usr/bin`` must match
|
|
241
|
+
each configured arch's class + machine (a 32-bit / aarch64 ``.so`` in an
|
|
242
|
+
x86_64 AppImage would only fail at ``dlopen`` time).
|
|
243
|
+
"""
|
|
244
|
+
declared = config.linux_required.binaries
|
|
245
|
+
if not declared:
|
|
246
|
+
return CheckResult("Native binaries", Status.SKIP, "not configured")
|
|
247
|
+
|
|
248
|
+
root = project_root.resolve()
|
|
249
|
+
missing = [
|
|
250
|
+
dep.source
|
|
251
|
+
for dep in declared
|
|
252
|
+
if not dep.source.startswith(("http://", "https://"))
|
|
253
|
+
and not (root / dep.source).is_file()
|
|
254
|
+
]
|
|
255
|
+
if missing:
|
|
256
|
+
return CheckResult(
|
|
257
|
+
"Native binaries",
|
|
258
|
+
Status.FAIL,
|
|
259
|
+
f"vendored source(s) not found: {', '.join(missing)}",
|
|
260
|
+
hint="build/vendor the artifact under the project and re-run "
|
|
261
|
+
"`kivyforge lock -p linux`.",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Statically-detectable staging collisions: two single-file (non-archive)
|
|
265
|
+
# sources whose basenames match land on the same usr/bin/<name> and the
|
|
266
|
+
# second silently clobbers the first. Archive-member collisions need
|
|
267
|
+
# extraction and are caught (loudly) at build time.
|
|
268
|
+
staged: dict[str, str] = {}
|
|
269
|
+
for dep in declared:
|
|
270
|
+
if dep.source.lower().endswith((".zip", ".tar.gz", ".tgz")):
|
|
271
|
+
continue
|
|
272
|
+
base = dep.source.rsplit("/", 1)[-1] or dep.name
|
|
273
|
+
prev = staged.get(base)
|
|
274
|
+
if prev is not None:
|
|
275
|
+
return CheckResult(
|
|
276
|
+
"Native binaries",
|
|
277
|
+
Status.FAIL,
|
|
278
|
+
f"{prev!r} and {dep.name!r} both stage usr/bin/{base}",
|
|
279
|
+
hint="two native binaries would overwrite each other in "
|
|
280
|
+
"usr/bin; rename one artifact/source so each stages to a "
|
|
281
|
+
"unique name.",
|
|
282
|
+
)
|
|
283
|
+
staged[base] = dep.name
|
|
284
|
+
|
|
285
|
+
archs = config.linux_required.archs
|
|
286
|
+
expected = {ARCH_ELF[a] for a in archs if a in ARCH_ELF}
|
|
287
|
+
bin_dir = root / "build" / "linux" / f"{config.display_name}.AppDir" / "usr" / "bin"
|
|
288
|
+
if expected and bin_dir.is_dir():
|
|
289
|
+
problems: list[str] = []
|
|
290
|
+
elves = 0
|
|
291
|
+
for p in sorted(bin_dir.rglob("*")):
|
|
292
|
+
if not is_elf(p):
|
|
293
|
+
continue
|
|
294
|
+
elves += 1
|
|
295
|
+
found = elf_machine(p)
|
|
296
|
+
if found not in expected:
|
|
297
|
+
want = ", ".join(sorted(describe(c, m) for c, m in expected))
|
|
298
|
+
problems.append(f"{p.name} is {describe(*found)}, need {want}")
|
|
299
|
+
if problems:
|
|
300
|
+
return CheckResult(
|
|
301
|
+
"Native binaries",
|
|
302
|
+
Status.FAIL,
|
|
303
|
+
"; ".join(problems),
|
|
304
|
+
hint="rebuild the native binaries for the configured arch "
|
|
305
|
+
f"({', '.join(archs)}), then re-lock.",
|
|
306
|
+
)
|
|
307
|
+
return CheckResult(
|
|
308
|
+
"Native binaries",
|
|
309
|
+
Status.PASS,
|
|
310
|
+
f"{elves} ELF match {', '.join(archs)}",
|
|
311
|
+
)
|
|
312
|
+
return CheckResult(
|
|
313
|
+
"Native binaries",
|
|
314
|
+
Status.PASS,
|
|
315
|
+
f"{len(declared)} declared; sources present (build to verify arch match)",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def check_linux_hosts_reachable(
|
|
320
|
+
probe: Probe, config: Config, lock: LinuxLockfile | None
|
|
321
|
+
) -> CheckResult:
|
|
322
|
+
hosts: set[str] = set()
|
|
323
|
+
if lock is not None:
|
|
324
|
+
for pkg in lock.packages:
|
|
325
|
+
for wheel in pkg.wheels:
|
|
326
|
+
if wheel.url:
|
|
327
|
+
_add_host(hosts, wheel.url)
|
|
328
|
+
for art in lock.python_runtime.artifacts:
|
|
329
|
+
if art.url:
|
|
330
|
+
_add_host(hosts, art.url)
|
|
331
|
+
for nb in lock.native_binaries:
|
|
332
|
+
if nb.url:
|
|
333
|
+
_add_host(hosts, nb.url)
|
|
334
|
+
# Packaging fetches the pinned build tools too, so their hosts matter.
|
|
335
|
+
for arch in config.linux_required.archs:
|
|
336
|
+
try:
|
|
337
|
+
_add_host(hosts, appimagetool_asset(arch)[0])
|
|
338
|
+
_add_host(hosts, type2_runtime_asset(arch)[0])
|
|
339
|
+
except AppDirError:
|
|
340
|
+
continue
|
|
341
|
+
if not hosts:
|
|
342
|
+
return CheckResult(
|
|
343
|
+
"Required hosts reachable", Status.PASS, "all artifacts vendored"
|
|
344
|
+
)
|
|
345
|
+
unreachable = [h for h in sorted(hosts) if not probe.tcp_reachable(h, 443)]
|
|
346
|
+
if unreachable:
|
|
347
|
+
return CheckResult(
|
|
348
|
+
"Required hosts reachable",
|
|
349
|
+
Status.FAIL,
|
|
350
|
+
f"unreachable: {', '.join(unreachable)}",
|
|
351
|
+
hint="check your network/proxy; these hosts serve pinned wheels, the "
|
|
352
|
+
"runtime, and the appimagetool/type2-runtime build tools.",
|
|
353
|
+
)
|
|
354
|
+
return CheckResult(
|
|
355
|
+
"Required hosts reachable", Status.PASS, ", ".join(sorted(hosts))
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _add_host(hosts: set[str], url: str) -> None:
|
|
360
|
+
netloc = urlparse(url).hostname
|
|
361
|
+
if netloc:
|
|
362
|
+
hosts.add(netloc)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def run_linux_checks(
|
|
366
|
+
probe: Probe,
|
|
367
|
+
*,
|
|
368
|
+
kivyforge_version: str,
|
|
369
|
+
config: Config | None = None,
|
|
370
|
+
project_root: Path | None = None,
|
|
371
|
+
lock: LinuxLockfile | None = None,
|
|
372
|
+
offline: bool = False,
|
|
373
|
+
) -> list[CheckResult]:
|
|
374
|
+
"""Run Linux doctor checks (linux-spec). Project checks SKIP without config."""
|
|
375
|
+
project_root = project_root or Path.cwd()
|
|
376
|
+
results = [
|
|
377
|
+
check_linux_host(probe),
|
|
378
|
+
check_gl_libraries(probe),
|
|
379
|
+
check_display_session(probe),
|
|
380
|
+
C.check_kivyforge_version(probe, kivyforge_version, offline=offline),
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
if config is None:
|
|
384
|
+
for name in (
|
|
385
|
+
"App source directory",
|
|
386
|
+
"glibc floor",
|
|
387
|
+
"Architecture coverage",
|
|
388
|
+
"App icon",
|
|
389
|
+
"Desktop entry valid",
|
|
390
|
+
"find_links directories",
|
|
391
|
+
"Native binaries",
|
|
392
|
+
"Required hosts reachable",
|
|
393
|
+
):
|
|
394
|
+
results.append(CheckResult(name, Status.SKIP, C.SKIP_NOTE))
|
|
395
|
+
return results
|
|
396
|
+
|
|
397
|
+
results += [
|
|
398
|
+
C.check_app_dir(config, project_root),
|
|
399
|
+
check_linux_glibc_floor(config, lock),
|
|
400
|
+
check_linux_arch_coverage(config, lock),
|
|
401
|
+
check_linux_app_icon(config, project_root),
|
|
402
|
+
check_linux_desktop_entry(probe, config),
|
|
403
|
+
check_linux_find_links(config, project_root),
|
|
404
|
+
check_linux_native_binaries(config, project_root),
|
|
405
|
+
check_linux_hosts_reachable(probe, config, lock),
|
|
406
|
+
]
|
|
407
|
+
return results
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def linux_doctor(
|
|
411
|
+
cwd: Path, *, kivyforge_version: str, offline: bool
|
|
412
|
+
) -> list[CheckResult]:
|
|
413
|
+
"""Load the Linux project + lock (if present) and run the Linux check set."""
|
|
414
|
+
pyproject = cwd / "pyproject.toml"
|
|
415
|
+
config = None
|
|
416
|
+
lock = None
|
|
417
|
+
parse_results: list[CheckResult] = []
|
|
418
|
+
if pyproject.is_file():
|
|
419
|
+
try:
|
|
420
|
+
config = load_config(pyproject, require_ios=False, require_linux=True)
|
|
421
|
+
except ConfigError as exc:
|
|
422
|
+
parse_results.append(
|
|
423
|
+
CheckResult("pyproject.toml", Status.FAIL, exc.format())
|
|
424
|
+
)
|
|
425
|
+
lockfile = cwd / "pylock.linux.toml"
|
|
426
|
+
if lockfile.is_file():
|
|
427
|
+
try:
|
|
428
|
+
lock = load(lockfile)
|
|
429
|
+
except LockError as exc:
|
|
430
|
+
parse_results.append(C.lock_parse_fail("linux", exc))
|
|
431
|
+
return parse_results + run_linux_checks(
|
|
432
|
+
RealProbe(),
|
|
433
|
+
kivyforge_version=kivyforge_version,
|
|
434
|
+
config=config,
|
|
435
|
+
project_root=cwd,
|
|
436
|
+
lock=lock,
|
|
437
|
+
offline=offline,
|
|
438
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""A tiny, hermetic ELF header reader (the ELF analog of ``machotools``).
|
|
2
|
+
|
|
3
|
+
``doctor`` uses this to confirm a staged native binary matches the AppImage's
|
|
4
|
+
target arch: a 32-bit or aarch64 ``.so`` in an x86_64 AppImage only fails at
|
|
5
|
+
``dlopen`` time with a cryptic message, so it is caught here from the header.
|
|
6
|
+
|
|
7
|
+
Pure-Python, no ``readelf`` dependency, so the check works on any host and in
|
|
8
|
+
unit tests. The endianness matters: ``e_machine`` is a 16-bit field whose byte
|
|
9
|
+
order is given by ``EI_DATA`` (``e_ident[5]``), so hard-coding little-endian
|
|
10
|
+
would misread a big-endian (mis-supplied) artifact — exactly the case this
|
|
11
|
+
check exists to catch.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import struct
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
ELF_MAGIC = b"\x7fELF"
|
|
20
|
+
|
|
21
|
+
# EI_CLASS (e_ident[4]).
|
|
22
|
+
ELFCLASS32 = 1
|
|
23
|
+
ELFCLASS64 = 2
|
|
24
|
+
|
|
25
|
+
# EI_DATA (e_ident[5]).
|
|
26
|
+
ELFDATA2LSB = 1
|
|
27
|
+
ELFDATA2MSB = 2
|
|
28
|
+
|
|
29
|
+
# e_machine values kivyforge maps its arches to.
|
|
30
|
+
EM_386 = 3
|
|
31
|
+
EM_X86_64 = 62
|
|
32
|
+
EM_AARCH64 = 183
|
|
33
|
+
|
|
34
|
+
# kivyforge arch -> (EI_CLASS, e_machine). aarch64 is deferred at the backend
|
|
35
|
+
# level but listed so the arch check is purely additive when it lands.
|
|
36
|
+
ARCH_ELF: dict[str, tuple[int, int]] = {
|
|
37
|
+
"x86_64": (ELFCLASS64, EM_X86_64),
|
|
38
|
+
"aarch64": (ELFCLASS64, EM_AARCH64),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_CLASS_NAMES = {ELFCLASS32: "ELF32", ELFCLASS64: "ELF64"}
|
|
42
|
+
_MACHINE_NAMES = {EM_386: "i386", EM_X86_64: "x86_64", EM_AARCH64: "aarch64"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ElfError(Exception):
|
|
46
|
+
"""The file is not a readable ELF header."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_elf(path: Path) -> bool:
|
|
50
|
+
"""True if *path* is a regular (non-symlink) file starting with ``\\x7fELF``.
|
|
51
|
+
|
|
52
|
+
Mirrors ``is_macho``: a non-ELF helper (e.g. a ``#!/bin/sh`` script) is
|
|
53
|
+
skipped by the arch check rather than failed for "not being x86_64".
|
|
54
|
+
"""
|
|
55
|
+
if not path.is_file() or path.is_symlink():
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
with path.open("rb") as fh:
|
|
59
|
+
head = fh.read(4)
|
|
60
|
+
except OSError:
|
|
61
|
+
return False
|
|
62
|
+
return head == ELF_MAGIC
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def elf_machine(path: Path) -> tuple[int, int]:
|
|
66
|
+
"""Return ``(EI_CLASS, e_machine)`` for the ELF at *path*.
|
|
67
|
+
|
|
68
|
+
Decodes the 16-bit ``e_machine`` (offset 18) using the endianness named by
|
|
69
|
+
``EI_DATA`` (offset 5). Raises :class:`ElfError` if *path* is not a readable
|
|
70
|
+
ELF header.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
with path.open("rb") as fh:
|
|
74
|
+
header = fh.read(20)
|
|
75
|
+
except OSError as exc:
|
|
76
|
+
raise ElfError(f"cannot read {path}: {exc}") from exc
|
|
77
|
+
if len(header) < 20 or header[:4] != ELF_MAGIC:
|
|
78
|
+
raise ElfError(f"{path} is not an ELF file")
|
|
79
|
+
ei_class = header[4]
|
|
80
|
+
ei_data = header[5]
|
|
81
|
+
if ei_data == ELFDATA2LSB:
|
|
82
|
+
endian = "<"
|
|
83
|
+
elif ei_data == ELFDATA2MSB:
|
|
84
|
+
endian = ">"
|
|
85
|
+
else:
|
|
86
|
+
raise ElfError(f"{path} has an invalid EI_DATA byte {ei_data!r}")
|
|
87
|
+
(e_machine,) = struct.unpack(f"{endian}H", header[18:20])
|
|
88
|
+
return ei_class, e_machine
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def describe(elf_class: int, e_machine: int) -> str:
|
|
92
|
+
"""A short human label for an ``(EI_CLASS, e_machine)`` pair."""
|
|
93
|
+
cls = _CLASS_NAMES.get(elf_class, f"class{elf_class}")
|
|
94
|
+
mach = _MACHINE_NAMES.get(e_machine, f"machine{e_machine}")
|
|
95
|
+
return f"{mach} ({cls})"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Generate the AppDir icon set (linux-spec).
|
|
2
|
+
|
|
3
|
+
Linux has no guaranteed host icon tool (the macOS ``sips``/``iconutil`` analog),
|
|
4
|
+
so when the project supplies ``[tool.kivy.linux.icons].source`` (a 1024×1024
|
|
5
|
+
PNG, validated like iOS/macOS) kivyforge resizes it with
|
|
6
|
+
[Pillow](https://python-pillow.org/) — the optional ``kivyforge[linux]`` extra —
|
|
7
|
+
into the freedesktop **hicolor** size set plus the root ``<app_id>.png``.
|
|
8
|
+
|
|
9
|
+
When no source is configured, a plain generated default icon is written instead
|
|
10
|
+
(using only the standard library) so ``appimagetool`` — which requires the icon
|
|
11
|
+
its ``.desktop`` ``Icon=`` key names — always has one to embed.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import shutil
|
|
17
|
+
import struct
|
|
18
|
+
import zlib
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from kivyforge.config.icons import validate_icon_source
|
|
22
|
+
from kivyforge.config.model import Config
|
|
23
|
+
|
|
24
|
+
from . import AppDirError
|
|
25
|
+
|
|
26
|
+
# freedesktop hicolor sizes to emit. The root AppDir icon is the 256px render.
|
|
27
|
+
HICOLOR_SIZES = (16, 32, 48, 64, 128, 256, 512)
|
|
28
|
+
ROOT_ICON_SIZE = 256
|
|
29
|
+
|
|
30
|
+
# The default icon's fill (a neutral slate), used when no source is configured.
|
|
31
|
+
_DEFAULT_RGBA = (45, 52, 64, 255)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def stage_icons(config: Config, project_root: Path, appdir: Path) -> bool:
|
|
35
|
+
"""Write the icon set into *appdir*; return ``True`` if a *user* icon was used.
|
|
36
|
+
|
|
37
|
+
With no configured source a generated default icon is written (returns
|
|
38
|
+
``False``) so the AppDir always carries the root ``<app_id>.png`` that
|
|
39
|
+
``appimagetool`` requires.
|
|
40
|
+
"""
|
|
41
|
+
app_id = config.linux_required.app_id
|
|
42
|
+
source = config.linux_required.icons.source
|
|
43
|
+
if not source:
|
|
44
|
+
_write_default(appdir, app_id)
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
src = (project_root / source).resolve()
|
|
48
|
+
validate_icon_source(src)
|
|
49
|
+
image = _open(src)
|
|
50
|
+
apps_root = appdir / "usr" / "share" / "icons" / "hicolor"
|
|
51
|
+
for size in HICOLOR_SIZES:
|
|
52
|
+
dest = apps_root / f"{size}x{size}" / "apps" / f"{app_id}.png"
|
|
53
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
_resize(image, size).save(dest, format="PNG")
|
|
55
|
+
|
|
56
|
+
root_icon = appdir / f"{app_id}.png"
|
|
57
|
+
_resize(image, ROOT_ICON_SIZE).save(root_icon, format="PNG")
|
|
58
|
+
_write_diricon(appdir, root_icon)
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _write_default(appdir: Path, app_id: str) -> None:
|
|
63
|
+
"""Emit a plain solid-colour root + 256px hicolor icon (no Pillow needed)."""
|
|
64
|
+
png = _solid_png(ROOT_ICON_SIZE, _DEFAULT_RGBA)
|
|
65
|
+
root_icon = appdir / f"{app_id}.png"
|
|
66
|
+
root_icon.write_bytes(png)
|
|
67
|
+
hicolor = appdir / "usr" / "share" / "icons" / "hicolor" / "256x256" / "apps"
|
|
68
|
+
hicolor.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
(hicolor / f"{app_id}.png").write_bytes(png)
|
|
70
|
+
_write_diricon(appdir, root_icon)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _write_diricon(appdir: Path, root_icon: Path) -> None:
|
|
74
|
+
"""Write ``.DirIcon`` (a copy of the root icon) at the AppDir root.
|
|
75
|
+
|
|
76
|
+
``.DirIcon`` is the freedesktop AppDir convention file managers and AppImage
|
|
77
|
+
consumers read to show the directory/app icon. ``appimagetool`` generates it
|
|
78
|
+
from ``Icon=`` at package time, but the ``-f folder`` artifact is consumed
|
|
79
|
+
directly, so kivyforge writes it too for a consistent icon across both. A
|
|
80
|
+
plain copy (not a symlink) keeps the folder artifact self-contained and
|
|
81
|
+
relocatable.
|
|
82
|
+
"""
|
|
83
|
+
shutil.copyfile(root_icon, appdir / ".DirIcon")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _solid_png(size: int, rgba: tuple[int, int, int, int]) -> bytes:
|
|
87
|
+
"""A minimal valid 8-bit RGBA PNG filled with a single colour."""
|
|
88
|
+
pixel = bytes(rgba)
|
|
89
|
+
row = b"\x00" + pixel * size # per-scanline filter byte (0) + pixels
|
|
90
|
+
idat = zlib.compress(row * size, 9)
|
|
91
|
+
|
|
92
|
+
def chunk(tag: bytes, data: bytes) -> bytes:
|
|
93
|
+
body = tag + data
|
|
94
|
+
crc = zlib.crc32(body) & 0xFFFFFFFF
|
|
95
|
+
return struct.pack(">I", len(data)) + body + struct.pack(">I", crc)
|
|
96
|
+
|
|
97
|
+
ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
|
|
98
|
+
return (
|
|
99
|
+
b"\x89PNG\r\n\x1a\n"
|
|
100
|
+
+ chunk(b"IHDR", ihdr)
|
|
101
|
+
+ chunk(b"IDAT", idat)
|
|
102
|
+
+ chunk(b"IEND", b"")
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _open(src: Path):
|
|
107
|
+
image_module = _pillow()
|
|
108
|
+
try:
|
|
109
|
+
return image_module.open(src).convert("RGBA")
|
|
110
|
+
except OSError as exc:
|
|
111
|
+
raise AppDirError(f"cannot read app icon {src}: {exc}") from exc
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _resize(image, size: int):
|
|
115
|
+
from PIL import Image
|
|
116
|
+
|
|
117
|
+
return image.resize((size, size), Image.LANCZOS)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _pillow():
|
|
121
|
+
try:
|
|
122
|
+
from PIL import Image
|
|
123
|
+
except ModuleNotFoundError as exc:
|
|
124
|
+
raise AppDirError(
|
|
125
|
+
"generating Linux icons needs Pillow, which is not installed.\n"
|
|
126
|
+
" Install the Linux extra: pip install 'kivyforge[linux]'\n"
|
|
127
|
+
" (or unset [tool.kivy.linux.icons].source to build without an icon)."
|
|
128
|
+
) from exc
|
|
129
|
+
return Image
|