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,480 @@
|
|
|
1
|
+
"""macOS-specific doctor checks (macos-spec).
|
|
2
|
+
|
|
3
|
+
Reuses the platform-neutral checks (pip, toolchain version, app source dir) from
|
|
4
|
+
``checks`` and adds the macOS environment (host + codesign) and project checks
|
|
5
|
+
(arch coverage, runtime floor, icon, find_links, reachable hosts) over the
|
|
6
|
+
``pylock.macos.toml`` model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
from kivyforge.config import ConfigError, load_config
|
|
15
|
+
from kivyforge.config.icons import APP_ICON_SIZE, icon_source_problem
|
|
16
|
+
from kivyforge.config.model import Config
|
|
17
|
+
from kivyforge.doctor import checks_common as C
|
|
18
|
+
from kivyforge.doctor.checks_common import _ver_tuple
|
|
19
|
+
from kivyforge.doctor.probe import Probe, RealProbe
|
|
20
|
+
from kivyforge.doctor.result import CheckResult, Status
|
|
21
|
+
from kivyforge.lock.find_links import find_links_doctor_detail
|
|
22
|
+
from kivyforge.lock.reader import LockError
|
|
23
|
+
|
|
24
|
+
from .lock import MacosLockfile, load, wheel_arch
|
|
25
|
+
from .machotools import is_macho, macho_arches
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_macos_host(probe: Probe) -> CheckResult:
|
|
29
|
+
host = probe.host_system()
|
|
30
|
+
if host != "Darwin":
|
|
31
|
+
return CheckResult(
|
|
32
|
+
"Host is macOS",
|
|
33
|
+
Status.FAIL,
|
|
34
|
+
f"host is {host!r}",
|
|
35
|
+
hint="the macOS .app backend (codesign + lipo) requires a macOS host.",
|
|
36
|
+
)
|
|
37
|
+
return CheckResult("Host is macOS", Status.PASS, "Darwin")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def check_codesign(probe: Probe) -> CheckResult:
|
|
41
|
+
if not probe.has_codesign():
|
|
42
|
+
return CheckResult(
|
|
43
|
+
"Codesign available",
|
|
44
|
+
Status.FAIL,
|
|
45
|
+
"codesign not found",
|
|
46
|
+
hint="install the Xcode command-line tools: `xcode-select --install`.",
|
|
47
|
+
)
|
|
48
|
+
return CheckResult("Codesign available", Status.PASS, "codesign present")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def check_macos_not_root(probe: Probe) -> CheckResult:
|
|
52
|
+
"""Apple DTS: signing as root/sudo mixes execution contexts and is a common
|
|
53
|
+
source of keychain errors (e.g. ``errSecInternalComponent``) — see
|
|
54
|
+
"Resolving errSecInternalComponent errors during code signing".
|
|
55
|
+
"""
|
|
56
|
+
if probe.is_root():
|
|
57
|
+
return CheckResult(
|
|
58
|
+
"Not running as root",
|
|
59
|
+
Status.WARN,
|
|
60
|
+
"running as root/sudo",
|
|
61
|
+
hint="codesign relies on the keychain, which is tied to a login "
|
|
62
|
+
"security context; running as root (or via sudo) often breaks "
|
|
63
|
+
"that context. Run `kivyforge package` as your normal user.",
|
|
64
|
+
)
|
|
65
|
+
return CheckResult("Not running as root", Status.PASS, "normal user")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_macos_app_icon(config: Config, project_root: Path) -> CheckResult:
|
|
69
|
+
source = config.macos_required.icons.source
|
|
70
|
+
if not source:
|
|
71
|
+
return CheckResult("App icon", Status.SKIP, "not configured")
|
|
72
|
+
problem = icon_source_problem((project_root / source).resolve())
|
|
73
|
+
if problem:
|
|
74
|
+
return CheckResult("App icon", Status.FAIL, f"{source} invalid", hint=problem)
|
|
75
|
+
return CheckResult("App icon", Status.PASS, f"{APP_ICON_SIZE}x{APP_ICON_SIZE} PNG")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def check_macos_find_links(config: Config, project_root: Path) -> CheckResult:
|
|
79
|
+
entries = config.macos_required.find_links
|
|
80
|
+
if not entries:
|
|
81
|
+
return CheckResult("find_links directories", Status.SKIP, "not configured")
|
|
82
|
+
root = project_root.resolve()
|
|
83
|
+
problems: list[tuple[str, str | None]] = []
|
|
84
|
+
warnings: list[tuple[str, str | None]] = []
|
|
85
|
+
ok: list[str] = []
|
|
86
|
+
for entry in entries:
|
|
87
|
+
path = (root / entry).resolve()
|
|
88
|
+
detail, hint = find_links_doctor_detail(root, entry, path, platform="macos")
|
|
89
|
+
if not path.is_dir():
|
|
90
|
+
problems.append((detail, hint))
|
|
91
|
+
elif not any(path.glob("*.whl")):
|
|
92
|
+
warnings.append((detail, hint))
|
|
93
|
+
else:
|
|
94
|
+
ok.append(detail)
|
|
95
|
+
if problems:
|
|
96
|
+
return CheckResult(
|
|
97
|
+
"find_links directories",
|
|
98
|
+
Status.FAIL,
|
|
99
|
+
"; ".join(d for d, _ in problems),
|
|
100
|
+
hint=next((h for _, h in problems if h), ""),
|
|
101
|
+
)
|
|
102
|
+
if warnings:
|
|
103
|
+
return CheckResult(
|
|
104
|
+
"find_links directories",
|
|
105
|
+
Status.WARN,
|
|
106
|
+
"; ".join(d for d, _ in warnings),
|
|
107
|
+
hint=next((h for _, h in warnings if h), ""),
|
|
108
|
+
)
|
|
109
|
+
return CheckResult("find_links directories", Status.PASS, "; ".join(ok))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def check_macos_arch_coverage(
|
|
113
|
+
config: Config, lock: MacosLockfile | None
|
|
114
|
+
) -> CheckResult:
|
|
115
|
+
if lock is None:
|
|
116
|
+
return CheckResult("Architecture coverage", Status.SKIP, "no pylock.macos.toml")
|
|
117
|
+
# macOS is arm64-only, so this is a single-arch check: every compiled
|
|
118
|
+
# dependency needs an arm64 or universal2 wheel (universal2 contains arm64).
|
|
119
|
+
target = config.macos_required.archs[0]
|
|
120
|
+
|
|
121
|
+
if target not in {a.arch for a in lock.python_runtime.artifacts}:
|
|
122
|
+
return CheckResult(
|
|
123
|
+
"Architecture coverage",
|
|
124
|
+
Status.FAIL,
|
|
125
|
+
f"runtime missing arch: {target}",
|
|
126
|
+
hint="re-run `kivyforge lock -p macos`.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
for pkg in lock.packages:
|
|
130
|
+
if any(w.is_pure_python for w in pkg.wheels):
|
|
131
|
+
continue
|
|
132
|
+
if not any(
|
|
133
|
+
wheel_arch(w.platform_tag) in ("universal2", target) for w in pkg.wheels
|
|
134
|
+
):
|
|
135
|
+
return CheckResult(
|
|
136
|
+
"Architecture coverage",
|
|
137
|
+
Status.FAIL,
|
|
138
|
+
f"{pkg.name} has no {target} wheel",
|
|
139
|
+
hint=f"a compiled dep needs a {target} or universal2 wheel; re-lock.",
|
|
140
|
+
)
|
|
141
|
+
return CheckResult("Architecture coverage", Status.PASS, f"all deps cover {target}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def check_macos_runtime_floor(
|
|
145
|
+
config: Config, lock: MacosLockfile | None
|
|
146
|
+
) -> CheckResult:
|
|
147
|
+
declared = config.macos_required.minimum_system_version
|
|
148
|
+
floor = lock.python_runtime.floor if lock else None
|
|
149
|
+
if not declared or not floor:
|
|
150
|
+
return CheckResult(
|
|
151
|
+
"Runtime floor", Status.SKIP, "no explicit minimum_system_version/floor"
|
|
152
|
+
)
|
|
153
|
+
if _ver_tuple(declared) < _ver_tuple(floor):
|
|
154
|
+
return CheckResult(
|
|
155
|
+
"Runtime floor",
|
|
156
|
+
Status.FAIL,
|
|
157
|
+
f"minimum_system_version {declared} < runtime floor {floor}",
|
|
158
|
+
hint=f"raise [tool.kivy.macos].minimum_system_version to >= {floor}.",
|
|
159
|
+
)
|
|
160
|
+
return CheckResult("Runtime floor", Status.PASS, f"{declared} >= {floor}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def check_macos_signing_identity(probe: Probe, config: Config) -> CheckResult:
|
|
164
|
+
"""When Developer ID signing is configured, the identity is in the keychain.
|
|
165
|
+
|
|
166
|
+
Also nudges towards Apple's "Care and Feeding of Developer ID" best practice
|
|
167
|
+
of keeping the identity out of ``login.keychain-db``: a dedicated keychain
|
|
168
|
+
isolates the private key from unrelated login-keychain churn (password
|
|
169
|
+
resets, iCloud Keychain sync, etc.) that can corrupt its ACL and produce a
|
|
170
|
+
hard-to-diagnose ``errSecInternalComponent`` from ``codesign`` (see FAQ).
|
|
171
|
+
"""
|
|
172
|
+
signing = config.macos_required.signing
|
|
173
|
+
if not signing.configured:
|
|
174
|
+
return CheckResult(
|
|
175
|
+
"Signing identity",
|
|
176
|
+
Status.SKIP,
|
|
177
|
+
"not configured (ad-hoc floor applies)",
|
|
178
|
+
)
|
|
179
|
+
identities = probe.keychain_identities()
|
|
180
|
+
matches = [line for line in identities if signing.identity in line]
|
|
181
|
+
if not matches:
|
|
182
|
+
return CheckResult(
|
|
183
|
+
"Signing identity",
|
|
184
|
+
Status.FAIL,
|
|
185
|
+
f"identity {signing.identity!r} not in keychain",
|
|
186
|
+
hint="import your 'Developer ID Application' certificate (Xcode -> "
|
|
187
|
+
"Settings -> Accounts -> Manage Certificates), or fix "
|
|
188
|
+
"[tool.kivy.macos.signing].identity.",
|
|
189
|
+
)
|
|
190
|
+
if len(matches) > 1:
|
|
191
|
+
return CheckResult(
|
|
192
|
+
"Signing identity",
|
|
193
|
+
Status.FAIL,
|
|
194
|
+
f"{len(matches)} certificates match {signing.identity!r} across your "
|
|
195
|
+
"keychains",
|
|
196
|
+
hint="codesign refuses to pick between identically-named "
|
|
197
|
+
"certificates in different keychains ('ambiguous'). Delete the "
|
|
198
|
+
"stale/duplicate copy (Keychain Access -> Certificates tab) so "
|
|
199
|
+
"only one remains.",
|
|
200
|
+
)
|
|
201
|
+
login_identities = probe.login_keychain_identities()
|
|
202
|
+
if any(signing.identity in line for line in login_identities):
|
|
203
|
+
return CheckResult(
|
|
204
|
+
"Signing identity",
|
|
205
|
+
Status.WARN,
|
|
206
|
+
f"{signing.identity} is in your login keychain",
|
|
207
|
+
hint="Apple recommends keeping Developer ID identities out of "
|
|
208
|
+
"login.keychain-db in a dedicated keychain, so unrelated login-"
|
|
209
|
+
"keychain issues can't corrupt the signing key. See FAQ.md "
|
|
210
|
+
"('macOS Developer ID signing fails with errSecInternalComponent') "
|
|
211
|
+
"for the migration steps.",
|
|
212
|
+
)
|
|
213
|
+
return CheckResult("Signing identity", Status.PASS, signing.identity)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def check_macos_signing_identity_type(config: Config) -> CheckResult:
|
|
217
|
+
"""Notarization only accepts a 'Developer ID Application' identity (or
|
|
218
|
+
'Developer ID Installer' for pkgs); other identity types (e.g. 'Apple
|
|
219
|
+
Development', used for day-to-day debugging) fail with "The binary is not
|
|
220
|
+
signed with a valid Developer ID certificate." — see Apple's "Resolving
|
|
221
|
+
common notarization issues".
|
|
222
|
+
"""
|
|
223
|
+
signing = config.macos_required.signing
|
|
224
|
+
if not signing.configured:
|
|
225
|
+
return CheckResult(
|
|
226
|
+
"Signing identity type",
|
|
227
|
+
Status.SKIP,
|
|
228
|
+
"not configured (ad-hoc floor applies)",
|
|
229
|
+
)
|
|
230
|
+
if (
|
|
231
|
+
"Developer ID Application" in signing.identity
|
|
232
|
+
or "Developer ID Installer" in signing.identity
|
|
233
|
+
):
|
|
234
|
+
return CheckResult("Signing identity type", Status.PASS, "Developer ID")
|
|
235
|
+
return CheckResult(
|
|
236
|
+
"Signing identity type",
|
|
237
|
+
Status.WARN,
|
|
238
|
+
f"{signing.identity!r} doesn't look like a Developer ID identity",
|
|
239
|
+
hint="notarization requires a 'Developer ID Application' (or "
|
|
240
|
+
"'Developer ID Installer') certificate — not 'Apple Development' or "
|
|
241
|
+
"another type, which is for local debugging only.",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def check_macos_notary_setup(probe: Probe, config: Config) -> CheckResult:
|
|
246
|
+
"""When a notary profile is configured, notarytool must be available.
|
|
247
|
+
|
|
248
|
+
Profile *validity* needs a network round-trip to Apple, so it is verified
|
|
249
|
+
at submit time; doctor only validates the local tooling.
|
|
250
|
+
"""
|
|
251
|
+
signing = config.macos_required.signing
|
|
252
|
+
if not signing.notary_profile:
|
|
253
|
+
return CheckResult("Notary setup", Status.SKIP, "no notary_profile")
|
|
254
|
+
if not probe.has_notarytool():
|
|
255
|
+
return CheckResult(
|
|
256
|
+
"Notary setup",
|
|
257
|
+
Status.FAIL,
|
|
258
|
+
"xcrun notarytool not found",
|
|
259
|
+
hint="notarytool ships with recent Xcode command-line tools; run "
|
|
260
|
+
"`xcode-select --install` / update the CLT.",
|
|
261
|
+
)
|
|
262
|
+
return CheckResult(
|
|
263
|
+
"Notary setup",
|
|
264
|
+
Status.PASS,
|
|
265
|
+
f"profile {signing.notary_profile!r}; notarytool present "
|
|
266
|
+
"(profile validity is checked at submit time)",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def check_macos_native_binaries(config: Config, project_root: Path) -> CheckResult:
|
|
271
|
+
"""Vendored native-binary sources exist and (once built) cover the archs.
|
|
272
|
+
|
|
273
|
+
SKIPs when no ``[tool.kivy.macos.native.binaries]`` are declared. Vendored
|
|
274
|
+
(repo-relative) sources must be present on disk; URL sources are re-checked
|
|
275
|
+
for reachability by ``check_macos_hosts_reachable``. No two single-file
|
|
276
|
+
(non-zip) sources may share a staged basename (they would overwrite each
|
|
277
|
+
other in ``bin/``; zip-member collisions are caught at build time). When the
|
|
278
|
+
bundle has been built, each Mach-O staged under ``Resources/bin`` must carry
|
|
279
|
+
the target arch.
|
|
280
|
+
"""
|
|
281
|
+
declared = config.macos_required.binaries
|
|
282
|
+
if not declared:
|
|
283
|
+
return CheckResult("Native binaries", Status.SKIP, "not configured")
|
|
284
|
+
|
|
285
|
+
root = project_root.resolve()
|
|
286
|
+
missing = [
|
|
287
|
+
dep.source
|
|
288
|
+
for dep in declared
|
|
289
|
+
if not dep.source.startswith(("http://", "https://"))
|
|
290
|
+
and not (root / dep.source).is_file()
|
|
291
|
+
]
|
|
292
|
+
if missing:
|
|
293
|
+
return CheckResult(
|
|
294
|
+
"Native binaries",
|
|
295
|
+
Status.FAIL,
|
|
296
|
+
f"vendored source(s) not found: {', '.join(missing)}",
|
|
297
|
+
hint="build/vendor the artifact under the project and re-run "
|
|
298
|
+
"`kivyforge lock -p macos`.",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Statically-detectable staging collisions: two single-file (non-zip)
|
|
302
|
+
# sources whose basenames match land on the same bin/<name> and the second
|
|
303
|
+
# silently clobbers the first. Zip-member collisions need extraction and are
|
|
304
|
+
# caught (loudly) at build time.
|
|
305
|
+
staged: dict[str, str] = {}
|
|
306
|
+
for dep in declared:
|
|
307
|
+
if dep.source.lower().endswith(".zip"):
|
|
308
|
+
continue
|
|
309
|
+
base = dep.source.rsplit("/", 1)[-1] or dep.name
|
|
310
|
+
prev = staged.get(base)
|
|
311
|
+
if prev is not None:
|
|
312
|
+
return CheckResult(
|
|
313
|
+
"Native binaries",
|
|
314
|
+
Status.FAIL,
|
|
315
|
+
f"{prev!r} and {dep.name!r} both stage bin/{base}",
|
|
316
|
+
hint="two native binaries would overwrite each other in "
|
|
317
|
+
"Contents/Resources/bin; rename one artifact/source so each "
|
|
318
|
+
"stages to a unique name.",
|
|
319
|
+
)
|
|
320
|
+
staged[base] = dep.name
|
|
321
|
+
|
|
322
|
+
target = config.macos_required.archs[0]
|
|
323
|
+
bin_dir = (
|
|
324
|
+
root
|
|
325
|
+
/ "build"
|
|
326
|
+
/ "macos"
|
|
327
|
+
/ f"{config.display_name}.app"
|
|
328
|
+
/ "Contents"
|
|
329
|
+
/ "Resources"
|
|
330
|
+
/ "bin"
|
|
331
|
+
)
|
|
332
|
+
if bin_dir.is_dir():
|
|
333
|
+
problems: list[str] = []
|
|
334
|
+
machos = 0
|
|
335
|
+
for p in sorted(bin_dir.rglob("*")):
|
|
336
|
+
if not is_macho(p):
|
|
337
|
+
continue
|
|
338
|
+
machos += 1
|
|
339
|
+
if target not in set(macho_arches(p)):
|
|
340
|
+
problems.append(f"{p.name} missing {target}")
|
|
341
|
+
if problems:
|
|
342
|
+
return CheckResult(
|
|
343
|
+
"Native binaries",
|
|
344
|
+
Status.FAIL,
|
|
345
|
+
"; ".join(problems),
|
|
346
|
+
hint=f"rebuild the native binaries for {target} "
|
|
347
|
+
f"(e.g. clang -arch {target}), then re-lock.",
|
|
348
|
+
)
|
|
349
|
+
return CheckResult(
|
|
350
|
+
"Native binaries",
|
|
351
|
+
Status.PASS,
|
|
352
|
+
f"{machos} Mach-O cover {target}",
|
|
353
|
+
)
|
|
354
|
+
return CheckResult(
|
|
355
|
+
"Native binaries",
|
|
356
|
+
Status.PASS,
|
|
357
|
+
f"{len(declared)} declared; sources present (build to verify arch coverage)",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def check_macos_hosts_reachable(
|
|
362
|
+
probe: Probe, lock: MacosLockfile | None
|
|
363
|
+
) -> CheckResult:
|
|
364
|
+
if lock is None:
|
|
365
|
+
return CheckResult(
|
|
366
|
+
"Required hosts reachable", Status.SKIP, "no pylock.macos.toml"
|
|
367
|
+
)
|
|
368
|
+
hosts: set[str] = set()
|
|
369
|
+
for pkg in lock.packages:
|
|
370
|
+
for wheel in pkg.wheels:
|
|
371
|
+
if wheel.url:
|
|
372
|
+
_add_host(hosts, wheel.url)
|
|
373
|
+
for art in lock.python_runtime.artifacts:
|
|
374
|
+
if art.url:
|
|
375
|
+
_add_host(hosts, art.url)
|
|
376
|
+
for nb in lock.native_binaries:
|
|
377
|
+
if nb.url:
|
|
378
|
+
_add_host(hosts, nb.url)
|
|
379
|
+
if not hosts:
|
|
380
|
+
return CheckResult(
|
|
381
|
+
"Required hosts reachable", Status.PASS, "all artifacts vendored"
|
|
382
|
+
)
|
|
383
|
+
unreachable = [h for h in sorted(hosts) if not probe.tcp_reachable(h, 443)]
|
|
384
|
+
if unreachable:
|
|
385
|
+
return CheckResult(
|
|
386
|
+
"Required hosts reachable",
|
|
387
|
+
Status.FAIL,
|
|
388
|
+
f"unreachable: {', '.join(unreachable)}",
|
|
389
|
+
hint="check your network/proxy; these hosts serve pinned artifacts.",
|
|
390
|
+
)
|
|
391
|
+
return CheckResult(
|
|
392
|
+
"Required hosts reachable", Status.PASS, ", ".join(sorted(hosts))
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _add_host(hosts: set[str], url: str) -> None:
|
|
397
|
+
netloc = urlparse(url).hostname
|
|
398
|
+
if netloc:
|
|
399
|
+
hosts.add(netloc)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def run_macos_checks(
|
|
403
|
+
probe: Probe,
|
|
404
|
+
*,
|
|
405
|
+
kivyforge_version: str,
|
|
406
|
+
config: Config | None = None,
|
|
407
|
+
project_root: Path | None = None,
|
|
408
|
+
lock: MacosLockfile | None = None,
|
|
409
|
+
offline: bool = False,
|
|
410
|
+
) -> list[CheckResult]:
|
|
411
|
+
"""Run macOS doctor checks (macos-spec). Project checks SKIP without config."""
|
|
412
|
+
project_root = project_root or Path.cwd()
|
|
413
|
+
results = [
|
|
414
|
+
check_macos_host(probe),
|
|
415
|
+
check_codesign(probe),
|
|
416
|
+
check_macos_not_root(probe),
|
|
417
|
+
C.check_pip_version(probe),
|
|
418
|
+
C.check_kivyforge_version(probe, kivyforge_version, offline=offline),
|
|
419
|
+
]
|
|
420
|
+
|
|
421
|
+
if config is None:
|
|
422
|
+
for name in (
|
|
423
|
+
"App source directory",
|
|
424
|
+
"Architecture coverage",
|
|
425
|
+
"Runtime floor",
|
|
426
|
+
"App icon",
|
|
427
|
+
"find_links directories",
|
|
428
|
+
"Native binaries",
|
|
429
|
+
"Signing identity",
|
|
430
|
+
"Signing identity type",
|
|
431
|
+
"Notary setup",
|
|
432
|
+
"Required hosts reachable",
|
|
433
|
+
):
|
|
434
|
+
results.append(CheckResult(name, Status.SKIP, C.SKIP_NOTE))
|
|
435
|
+
return results
|
|
436
|
+
|
|
437
|
+
results += [
|
|
438
|
+
C.check_app_dir(config, project_root),
|
|
439
|
+
check_macos_arch_coverage(config, lock),
|
|
440
|
+
check_macos_runtime_floor(config, lock),
|
|
441
|
+
check_macos_app_icon(config, project_root),
|
|
442
|
+
check_macos_find_links(config, project_root),
|
|
443
|
+
check_macos_native_binaries(config, project_root),
|
|
444
|
+
check_macos_signing_identity(probe, config),
|
|
445
|
+
check_macos_signing_identity_type(config),
|
|
446
|
+
check_macos_notary_setup(probe, config),
|
|
447
|
+
check_macos_hosts_reachable(probe, lock),
|
|
448
|
+
]
|
|
449
|
+
return results
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def macos_doctor(
|
|
453
|
+
cwd: Path, *, kivyforge_version: str, offline: bool
|
|
454
|
+
) -> list[CheckResult]:
|
|
455
|
+
"""Load the macOS project + lock (if present) and run the macOS check set."""
|
|
456
|
+
pyproject = cwd / "pyproject.toml"
|
|
457
|
+
config = None
|
|
458
|
+
lock = None
|
|
459
|
+
parse_results: list[CheckResult] = []
|
|
460
|
+
if pyproject.is_file():
|
|
461
|
+
try:
|
|
462
|
+
config = load_config(pyproject, require_ios=False, require_macos=True)
|
|
463
|
+
except ConfigError as exc:
|
|
464
|
+
parse_results.append(
|
|
465
|
+
CheckResult("pyproject.toml", Status.FAIL, exc.format())
|
|
466
|
+
)
|
|
467
|
+
lockfile = cwd / "pylock.macos.toml"
|
|
468
|
+
if lockfile.is_file():
|
|
469
|
+
try:
|
|
470
|
+
lock = load(lockfile)
|
|
471
|
+
except LockError as exc:
|
|
472
|
+
parse_results.append(C.lock_parse_fail("macos", exc))
|
|
473
|
+
return parse_results + run_macos_checks(
|
|
474
|
+
RealProbe(),
|
|
475
|
+
kivyforge_version=kivyforge_version,
|
|
476
|
+
config=config,
|
|
477
|
+
project_root=cwd,
|
|
478
|
+
lock=lock,
|
|
479
|
+
offline=offline,
|
|
480
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Generate a macOS ``.icns`` from a 1024×1024 PNG source (macos-spec).
|
|
2
|
+
|
|
3
|
+
Uses the system ``sips`` (resize) + ``iconutil`` (iconset → icns), so no image
|
|
4
|
+
libraries are needed. The source is validated as a 1024×1024 PNG first, matching
|
|
5
|
+
the iOS icon discipline.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from kivyforge.config.icons import IconSourceError, validate_icon_source
|
|
16
|
+
|
|
17
|
+
from . import AppBundleError
|
|
18
|
+
|
|
19
|
+
# (pixel size, iconset filename) pairs Apple's iconutil expects. Retina (@2x)
|
|
20
|
+
# variants are rendered at 2× the point size from the single 1024² source.
|
|
21
|
+
_ICONSET = [
|
|
22
|
+
(16, "icon_16x16.png"),
|
|
23
|
+
(32, "icon_16x16@2x.png"),
|
|
24
|
+
(32, "icon_32x32.png"),
|
|
25
|
+
(64, "icon_32x32@2x.png"),
|
|
26
|
+
(128, "icon_128x128.png"),
|
|
27
|
+
(256, "icon_128x128@2x.png"),
|
|
28
|
+
(256, "icon_256x256.png"),
|
|
29
|
+
(512, "icon_256x256@2x.png"),
|
|
30
|
+
(512, "icon_512x512.png"),
|
|
31
|
+
(1024, "icon_512x512@2x.png"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def generate_icns(source: Path, dest: Path) -> None:
|
|
36
|
+
"""Render *source* (1024×1024 PNG) into an ``.icns`` at *dest*."""
|
|
37
|
+
validate_icon_source(source)
|
|
38
|
+
with tempfile.TemporaryDirectory(prefix="kivy-icns-") as tmp:
|
|
39
|
+
iconset = Path(tmp) / "icon.iconset"
|
|
40
|
+
iconset.mkdir()
|
|
41
|
+
for size, name in _ICONSET:
|
|
42
|
+
_sips_resize(source, iconset / name, size)
|
|
43
|
+
_iconutil(iconset, dest)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _sips_resize(source: Path, out: Path, size: int) -> None:
|
|
47
|
+
_run(
|
|
48
|
+
[
|
|
49
|
+
"sips",
|
|
50
|
+
"-z",
|
|
51
|
+
str(size),
|
|
52
|
+
str(size),
|
|
53
|
+
str(source),
|
|
54
|
+
"--out",
|
|
55
|
+
str(out),
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _iconutil(iconset: Path, dest: Path) -> None:
|
|
61
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
_run(["iconutil", "-c", "icns", str(iconset), "-o", str(dest)])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run(cmd: list[str]) -> None:
|
|
66
|
+
if shutil.which(cmd[0]) is None:
|
|
67
|
+
raise AppBundleError(
|
|
68
|
+
f"required macOS tool {cmd[0]!r} not found (ships with macOS)."
|
|
69
|
+
)
|
|
70
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
71
|
+
if proc.returncode != 0:
|
|
72
|
+
raise AppBundleError(f"{cmd[0]} failed: {(proc.stderr or proc.stdout).strip()}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = ["AppBundleError", "IconSourceError", "generate_icns"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""The ``.app`` launcher — a compiled Mach-O stub (the iOS ``main.m`` analog).
|
|
2
|
+
|
|
3
|
+
The bundle's ``CFBundleExecutable`` must be a real Mach-O binary, not a shell
|
|
4
|
+
script: Finder/LaunchServices treats a text-script main executable as a document
|
|
5
|
+
and spawns a Terminal/console window for it, and a script also can't be
|
|
6
|
+
ad-hoc-signed as the app's main image. So kivyforge compiles a tiny C launcher
|
|
7
|
+
(with ``clang`` from the Xcode command-line tools, which are already required for
|
|
8
|
+
``codesign``). At runtime it locates the bundle relative to itself — so the
|
|
9
|
+
``.app`` stays fully relocatable — points the bundled CPython at its own home +
|
|
10
|
+
the app's ``lib``/``app`` directories, and ``execv``s the entry-point module.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import subprocess
|
|
16
|
+
import tempfile
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from . import AppBundleError
|
|
20
|
+
|
|
21
|
+
# The launcher is intentionally minimal: resolve its own path, derive the bundle
|
|
22
|
+
# layout, set the Python environment, and exec the interpreter on the entry
|
|
23
|
+
# script. ``{entry}`` is filled with the (identifier-only) entry-point name.
|
|
24
|
+
_SOURCE = r"""
|
|
25
|
+
#include <stdio.h>
|
|
26
|
+
#include <stdlib.h>
|
|
27
|
+
#include <string.h>
|
|
28
|
+
#include <limits.h>
|
|
29
|
+
#include <libgen.h>
|
|
30
|
+
#include <unistd.h>
|
|
31
|
+
#include <mach-o/dyld.h>
|
|
32
|
+
|
|
33
|
+
static const char *ENTRY = "{entry}";
|
|
34
|
+
|
|
35
|
+
int main(int argc, char *argv[]) {{
|
|
36
|
+
char raw[PATH_MAX];
|
|
37
|
+
uint32_t size = sizeof(raw);
|
|
38
|
+
if (_NSGetExecutablePath(raw, &size) != 0) return 71;
|
|
39
|
+
char exe[PATH_MAX];
|
|
40
|
+
if (realpath(raw, exe) == NULL) return 71; /* .../Contents/MacOS/<name> */
|
|
41
|
+
|
|
42
|
+
char a[PATH_MAX];
|
|
43
|
+
strncpy(a, exe, sizeof(a)); a[sizeof(a) - 1] = 0;
|
|
44
|
+
char *macos = dirname(a); /* .../Contents/MacOS */
|
|
45
|
+
char b[PATH_MAX];
|
|
46
|
+
strncpy(b, macos, sizeof(b)); b[sizeof(b) - 1] = 0;
|
|
47
|
+
char *contents = dirname(b); /* .../Contents */
|
|
48
|
+
|
|
49
|
+
char home[PATH_MAX], app[PATH_MAX], lib[PATH_MAX], bin[PATH_MAX];
|
|
50
|
+
char py[PATH_MAX], script[PATH_MAX], pypath[2 * PATH_MAX];
|
|
51
|
+
snprintf(home, sizeof(home), "%s/Resources/python", contents);
|
|
52
|
+
snprintf(app, sizeof(app), "%s/Resources/app", contents);
|
|
53
|
+
snprintf(lib, sizeof(lib), "%s/Resources/lib", contents);
|
|
54
|
+
snprintf(bin, sizeof(bin), "%s/Resources/bin", contents);
|
|
55
|
+
snprintf(py, sizeof(py), "%s/bin/python3", home);
|
|
56
|
+
snprintf(script, sizeof(script), "%s/%s.py", app, ENTRY);
|
|
57
|
+
snprintf(pypath, sizeof(pypath), "%s:%s", app, lib);
|
|
58
|
+
|
|
59
|
+
setenv("PYTHONHOME", home, 1);
|
|
60
|
+
setenv("PYTHONPATH", pypath, 1);
|
|
61
|
+
/* Isolate from ~/.local and user site config; the bundle is self-contained. */
|
|
62
|
+
setenv("PYTHONNOUSERSITE", "1", 1);
|
|
63
|
+
/* Prepend Resources/bin so user-declared native helper executables resolve by
|
|
64
|
+
name (subprocess/PATH lookups). Harmless when the directory is absent. */
|
|
65
|
+
const char *old_path = getenv("PATH");
|
|
66
|
+
char newpath[3 * PATH_MAX];
|
|
67
|
+
if (old_path != NULL && old_path[0] != 0) {{
|
|
68
|
+
snprintf(newpath, sizeof(newpath), "%s:%s", bin, old_path);
|
|
69
|
+
}} else {{
|
|
70
|
+
snprintf(newpath, sizeof(newpath), "%s", bin);
|
|
71
|
+
}}
|
|
72
|
+
setenv("PATH", newpath, 1);
|
|
73
|
+
chdir(app);
|
|
74
|
+
|
|
75
|
+
char **child = (char **)malloc(sizeof(char *) * (argc + 2));
|
|
76
|
+
if (child == NULL) return 71;
|
|
77
|
+
child[0] = py;
|
|
78
|
+
child[1] = script;
|
|
79
|
+
for (int i = 1; i < argc; i++) child[i + 1] = argv[i];
|
|
80
|
+
child[argc + 1] = NULL;
|
|
81
|
+
execv(py, child);
|
|
82
|
+
perror("kivyforge launcher: execv");
|
|
83
|
+
return 71;
|
|
84
|
+
}}
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def render_launcher_source(entry_point: str) -> str:
|
|
89
|
+
"""The C source for a launcher that execs *entry_point*.py (pure/testable)."""
|
|
90
|
+
if not entry_point.isidentifier():
|
|
91
|
+
raise AppBundleError(f"entry_point {entry_point!r} is not a valid module name.")
|
|
92
|
+
return _SOURCE.format(entry=entry_point)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_launcher(dest: Path, *, entry_point: str, arch: str) -> None:
|
|
96
|
+
"""Compile the launcher for *arch* to *dest*."""
|
|
97
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
source = render_launcher_source(entry_point)
|
|
99
|
+
with tempfile.TemporaryDirectory(prefix="kivy-launcher-") as tmp:
|
|
100
|
+
src = Path(tmp) / "launcher.c"
|
|
101
|
+
src.write_text(source, encoding="utf-8")
|
|
102
|
+
_compile(["clang", "-O2", "-Wall", "-arch", arch, "-o", str(dest), str(src)])
|
|
103
|
+
dest.chmod(0o755)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _compile(cmd: list[str]) -> None:
|
|
107
|
+
try:
|
|
108
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
109
|
+
except FileNotFoundError as exc:
|
|
110
|
+
raise AppBundleError(
|
|
111
|
+
"clang not found; the launcher needs the Xcode command-line tools.\n"
|
|
112
|
+
" Install them with: xcode-select --install"
|
|
113
|
+
) from exc
|
|
114
|
+
if proc.returncode != 0:
|
|
115
|
+
raise AppBundleError(
|
|
116
|
+
f"failed to compile the app launcher: "
|
|
117
|
+
f"{(proc.stderr or proc.stdout).strip()}"
|
|
118
|
+
)
|