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,507 @@
|
|
|
1
|
+
"""Deterministically build (and vendor / verify) the Windows launcher .exe.
|
|
2
|
+
|
|
3
|
+
The launcher is **compiled once in CI, never on user or dev machines** — the
|
|
4
|
+
onedir bundle ships the prebuilt, vendored binary. This script is the source of
|
|
5
|
+
that binary and its reproducibility gate:
|
|
6
|
+
|
|
7
|
+
python -m kivyforge.platforms.windows.launcher.build_launcher build
|
|
8
|
+
Compile launcher.c and (over)write the vendored binary + its SHA-256 in
|
|
9
|
+
the manifest. Run locally when the C source changes.
|
|
10
|
+
|
|
11
|
+
python -m kivyforge.platforms.windows.launcher.build_launcher verify
|
|
12
|
+
Recompile to a temp dir and byte-compare against the vendored binary,
|
|
13
|
+
failing on any drift. This is the CI reproducibility check.
|
|
14
|
+
|
|
15
|
+
Determinism, in three flags: ``/Brepro`` on both the compile and link steps
|
|
16
|
+
replaces the PE timestamp with a content hash (no wall-clock time), the linker
|
|
17
|
+
``/RELEASE`` writes a deterministic checksum, and ``/EMITTOOLVERSIONINFO:NO``
|
|
18
|
+
omits the PE **"rich header"** — the block stamping the exact build of every
|
|
19
|
+
tool that touched the image. kivyforge's launcher **appends nothing** to the
|
|
20
|
+
binary, so there is no zip/timestamp tail to non-determinize it (unlike
|
|
21
|
+
distlib).
|
|
22
|
+
|
|
23
|
+
``/EMITTOOLVERSIONINFO:NO`` is what makes the binary reproducible *across
|
|
24
|
+
machines*, and it is not optional: ``VCTOOLSVERSION`` (the toolset directory
|
|
25
|
+
name, e.g. ``14.51.36231``) does **not** identify the compiler shipped inside
|
|
26
|
+
it — ``cl`` there reports a different build (e.g. ``19.51.36248``), and Microsoft
|
|
27
|
+
services that binary in place. Two hosted runner images can therefore expose
|
|
28
|
+
the same ``VCTOOLSVERSION`` while running ``cl``/``link`` builds ``36248`` and
|
|
29
|
+
``36252``. Those builds land in the rich header, whose length shifts every
|
|
30
|
+
following file offset and perturbs the ``/Brepro`` hash: 307 bytes of drift with
|
|
31
|
+
*byte-identical machine code*. No pin can fix that (vcvars selects by directory
|
|
32
|
+
version, which is the thing that doesn't discriminate) — so drop the stamp.
|
|
33
|
+
|
|
34
|
+
The toolset pin below is still worth keeping, but for the other half of the
|
|
35
|
+
problem: a genuinely different MSVC or Windows SDK can emit different *code*.
|
|
36
|
+
|
|
37
|
+
Windows-only (needs ``cl``/``link`` from Visual Studio Build Tools).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import argparse
|
|
43
|
+
import hashlib
|
|
44
|
+
import os
|
|
45
|
+
import shutil
|
|
46
|
+
import subprocess
|
|
47
|
+
import sys
|
|
48
|
+
import tempfile
|
|
49
|
+
from dataclasses import dataclass
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
HERE = Path(__file__).resolve().parent
|
|
53
|
+
SOURCE = HERE / "launcher.c"
|
|
54
|
+
VENDOR_DIR = HERE.parent / "vendor"
|
|
55
|
+
# arm64: launcher.c is portable, so arm64 reuses the same source but needs a
|
|
56
|
+
# SECOND vendored binary (launcher-arm64.exe) + SHA256SUMS entry. Make these
|
|
57
|
+
# per-arch and thread arch through compile_launcher/_vcvars_env/do_build/do_verify.
|
|
58
|
+
# Cross-compile from an x64 runner via `vcvarsall x64_arm64`. See arm64-windows.md §5.
|
|
59
|
+
LAUNCHER_NAME = "launcher-amd64.exe"
|
|
60
|
+
VENDORED_LAUNCHER = VENDOR_DIR / LAUNCHER_NAME
|
|
61
|
+
MANIFEST = VENDOR_DIR / "SHA256SUMS"
|
|
62
|
+
# The MSVC + Windows SDK pin that produced the vendored binary: build and CI
|
|
63
|
+
# verify load them via ``vcvarsall <arch> <winsdk> -vcvars_ver=<tools>``.
|
|
64
|
+
#
|
|
65
|
+
# ``TOOLSET.txt`` format (one or two lines, ``#`` comments allowed)::
|
|
66
|
+
#
|
|
67
|
+
# 14.51.36231 # full VCTOOLSVERSION (required)
|
|
68
|
+
# 10.0.26100.0 # WindowsSDKVersion (optional but recorded by build)
|
|
69
|
+
#
|
|
70
|
+
# This pin holds *code generation* steady — a different MSVC or SDK can inline
|
|
71
|
+
# different CRT/SDK code and change the emitted instructions. It deliberately
|
|
72
|
+
# does **not** carry the burden of pinning the compiler *identity*: the module
|
|
73
|
+
# docstring explains why it cannot (VCTOOLSVERSION names a directory, not the
|
|
74
|
+
# serviced ``cl`` inside it), which is what ``/EMITTOOLVERSIONINFO:NO`` handles
|
|
75
|
+
# instead. Pin the *full* VCTOOLSVERSION rather than major.minor, though:
|
|
76
|
+
# ``-vcvars_ver=14.51`` means "latest installed 14.51.xxxxx". When a runner
|
|
77
|
+
# image drops either pin, verify fails loudly and ``revendor_launcher`` re-pins
|
|
78
|
+
# against the new defaults.
|
|
79
|
+
TOOLSET_FILE = VENDOR_DIR / "TOOLSET.txt"
|
|
80
|
+
|
|
81
|
+
# Deterministic compile/link flags. Keep these lists in lockstep with any change
|
|
82
|
+
# that would alter the emitted bytes (both here and in the CI workflow); every
|
|
83
|
+
# change to them requires a re-vendor.
|
|
84
|
+
_CL_FLAGS = ["/nologo", "/c", "/O1", "/Brepro", "/utf-8", "/DUNICODE", "/D_UNICODE"]
|
|
85
|
+
_LINK_FLAGS = ["/nologo", "/Brepro", "/RELEASE", "/EMITTOOLVERSIONINFO:NO"]
|
|
86
|
+
_LINK_LIBS = ["kernel32.lib", "shell32.lib"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class LauncherBuildError(Exception):
|
|
90
|
+
"""The launcher could not be compiled or failed its reproducibility check."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class ToolsetPin:
|
|
95
|
+
"""The exact MSVC + Windows SDK pair a launcher binary was built with."""
|
|
96
|
+
|
|
97
|
+
vc_tools: str
|
|
98
|
+
winsdk: str | None = None
|
|
99
|
+
|
|
100
|
+
def label(self) -> str:
|
|
101
|
+
return (
|
|
102
|
+
f"MSVC {self.vc_tools} + Windows SDK {self.winsdk}"
|
|
103
|
+
if self.winsdk
|
|
104
|
+
else f"MSVC {self.vc_tools}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def pinned_toolset() -> ToolsetPin | None:
|
|
109
|
+
"""The pinned MSVC (+ optional Windows SDK), or ``None`` if unrecorded.
|
|
110
|
+
|
|
111
|
+
Accepts legacy single-line ``major.minor`` / full-version-only pins;
|
|
112
|
+
``do_build`` rewrites them to the two-line full form on the next re-vendor.
|
|
113
|
+
"""
|
|
114
|
+
if not TOOLSET_FILE.is_file():
|
|
115
|
+
return None
|
|
116
|
+
lines = [
|
|
117
|
+
line.strip()
|
|
118
|
+
for line in TOOLSET_FILE.read_text(encoding="utf-8").splitlines()
|
|
119
|
+
if line.strip() and not line.lstrip().startswith("#")
|
|
120
|
+
]
|
|
121
|
+
if not lines:
|
|
122
|
+
return None
|
|
123
|
+
vc_tools = lines[0]
|
|
124
|
+
winsdk = lines[1] if len(lines) > 1 else None
|
|
125
|
+
return ToolsetPin(vc_tools, winsdk)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def write_toolset_pin(pin: ToolsetPin) -> None:
|
|
129
|
+
"""Persist *pin* so CI verify can reload the exact same vcvars pair."""
|
|
130
|
+
lines = [
|
|
131
|
+
"# MSVC VCTOOLSVERSION + Windows SDK used to build launcher-amd64.exe.",
|
|
132
|
+
"# Re-vendor via the `revendor_launcher` CI workflow when either drifts.",
|
|
133
|
+
pin.vc_tools,
|
|
134
|
+
]
|
|
135
|
+
if pin.winsdk:
|
|
136
|
+
lines.append(pin.winsdk)
|
|
137
|
+
TOOLSET_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _vc_tools_version(env: dict[str, str]) -> str:
|
|
141
|
+
"""The ``VCTOOLSVERSION`` from a vcvars environment, or empty if unset."""
|
|
142
|
+
return env.get("VCTOOLSVERSION", "").strip()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _windows_sdk_version(env: dict[str, str]) -> str:
|
|
146
|
+
"""The ``WindowsSDKVersion`` from a vcvars environment, trailing ``\\`` stripped."""
|
|
147
|
+
return env.get("WINDOWSSDKVERSION", "").strip().strip("\\")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _vcvars_env(
|
|
151
|
+
arch: str = "x64",
|
|
152
|
+
*,
|
|
153
|
+
vcvars_ver: str | None = None,
|
|
154
|
+
winsdk_version: str | None = None,
|
|
155
|
+
) -> dict[str, str]:
|
|
156
|
+
"""Return the environment after loading the Visual Studio dev vars for *arch*.
|
|
157
|
+
|
|
158
|
+
Locates the toolset with ``vswhere`` and runs ``vcvarsall.bat``, capturing
|
|
159
|
+
the resulting environment so ``cl``/``link`` resolve without the caller
|
|
160
|
+
being inside a Developer Command Prompt. ``vcvars_ver`` pins the MSVC
|
|
161
|
+
toolset (full ``VCTOOLSVERSION``, e.g. ``14.51.36231``); ``winsdk_version``
|
|
162
|
+
pins the Windows SDK (e.g. ``10.0.26100.0``).
|
|
163
|
+
"""
|
|
164
|
+
program_files_x86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
|
165
|
+
vswhere = (
|
|
166
|
+
Path(program_files_x86)
|
|
167
|
+
/ "Microsoft Visual Studio"
|
|
168
|
+
/ "Installer"
|
|
169
|
+
/ "vswhere.exe"
|
|
170
|
+
)
|
|
171
|
+
if not vswhere.is_file():
|
|
172
|
+
raise LauncherBuildError(
|
|
173
|
+
f"vswhere not found at {vswhere}; install Visual Studio (with the "
|
|
174
|
+
"C++ build tools) to build the launcher."
|
|
175
|
+
)
|
|
176
|
+
# arm64: cross-compiling the arm64 launcher needs the ARM64 toolset — also
|
|
177
|
+
# require "Microsoft.VisualStudio.Component.VC.Tools.ARM64" and load vcvars
|
|
178
|
+
# with arch="x64_arm64". See arm64-windows.md §5.
|
|
179
|
+
proc = subprocess.run(
|
|
180
|
+
[
|
|
181
|
+
str(vswhere),
|
|
182
|
+
"-latest",
|
|
183
|
+
"-products",
|
|
184
|
+
"*",
|
|
185
|
+
"-requires",
|
|
186
|
+
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
|
187
|
+
"-property",
|
|
188
|
+
"installationPath",
|
|
189
|
+
],
|
|
190
|
+
capture_output=True,
|
|
191
|
+
text=True,
|
|
192
|
+
)
|
|
193
|
+
install = proc.stdout.strip().splitlines()
|
|
194
|
+
if not install:
|
|
195
|
+
raise LauncherBuildError(
|
|
196
|
+
"no Visual Studio install with the C++ x64 toolset was found "
|
|
197
|
+
"(vswhere returned nothing)."
|
|
198
|
+
)
|
|
199
|
+
vcvarsall = Path(install[0]) / "VC" / "Auxiliary" / "Build" / "vcvarsall.bat"
|
|
200
|
+
if not vcvarsall.is_file():
|
|
201
|
+
raise LauncherBuildError(f"vcvarsall.bat not found at {vcvarsall}.")
|
|
202
|
+
|
|
203
|
+
# Run vcvarsall then `set`, and parse the exported environment back out.
|
|
204
|
+
# A temp batch file avoids the nested-quote mangling that breaks passing the
|
|
205
|
+
# whole compound command (with a quoted, space-containing path) as one arg.
|
|
206
|
+
# Positional form: vcvarsall <arch> [winsdk_version] [-vcvars_ver=...].
|
|
207
|
+
marker = "__KIVYFORGE_ENV__"
|
|
208
|
+
sdk_arg = f" {winsdk_version}" if winsdk_version else ""
|
|
209
|
+
ver_arg = f" -vcvars_ver={vcvars_ver}" if vcvars_ver else ""
|
|
210
|
+
with tempfile.TemporaryDirectory(prefix="kivy-vcvars-") as tmp:
|
|
211
|
+
bat = Path(tmp) / "env.bat"
|
|
212
|
+
bat.write_text(
|
|
213
|
+
f'@echo off\r\ncall "{vcvarsall}" {arch}{sdk_arg}{ver_arg} >nul\r\n'
|
|
214
|
+
f"echo {marker}\r\nset\r\n",
|
|
215
|
+
encoding="utf-8",
|
|
216
|
+
)
|
|
217
|
+
result = subprocess.run(["cmd", "/c", str(bat)], capture_output=True, text=True)
|
|
218
|
+
if result.returncode != 0 or marker not in result.stdout:
|
|
219
|
+
raise LauncherBuildError(
|
|
220
|
+
f"failed to initialize the MSVC environment via {vcvarsall}"
|
|
221
|
+
f" (arch={arch}, winsdk={winsdk_version or 'latest'}, "
|
|
222
|
+
f"vcvars_ver={vcvars_ver or 'latest'}):\n"
|
|
223
|
+
f"{result.stdout}\n{result.stderr}"
|
|
224
|
+
)
|
|
225
|
+
env: dict[str, str] = {}
|
|
226
|
+
seen_marker = False
|
|
227
|
+
for line in result.stdout.splitlines():
|
|
228
|
+
if not seen_marker:
|
|
229
|
+
if line.strip() == marker:
|
|
230
|
+
seen_marker = True
|
|
231
|
+
continue
|
|
232
|
+
if "=" in line:
|
|
233
|
+
key, _, value = line.partition("=")
|
|
234
|
+
# Uppercase keys so PATH is unambiguous when merged over os.environ
|
|
235
|
+
# (cmd's `set` prints `Path=...`, os.environ has `PATH=...`).
|
|
236
|
+
env[key.upper()] = value
|
|
237
|
+
return env
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def compile_c_source(
|
|
241
|
+
source: Path,
|
|
242
|
+
dest: Path,
|
|
243
|
+
*,
|
|
244
|
+
subsystem: str = "WINDOWS",
|
|
245
|
+
extra_libs: tuple[str, ...] = (),
|
|
246
|
+
arch: str = "x64",
|
|
247
|
+
vcvars_ver: str | None = None,
|
|
248
|
+
winsdk_version: str | None = None,
|
|
249
|
+
) -> Path:
|
|
250
|
+
"""Deterministically compile a single C *source* to *dest* with MSVC.
|
|
251
|
+
|
|
252
|
+
Shared by the launcher build and the launcher end-to-end test (which builds
|
|
253
|
+
a tiny console stub standing in for ``python.exe``). ``subsystem`` links
|
|
254
|
+
``/SUBSYSTEM:<subsystem>``; ``extra_libs`` appends import libraries;
|
|
255
|
+
``vcvars_ver`` / ``winsdk_version`` pin the MSVC toolset and Windows SDK
|
|
256
|
+
for byte-reproducibility.
|
|
257
|
+
"""
|
|
258
|
+
if sys.platform != "win32":
|
|
259
|
+
raise LauncherBuildError(
|
|
260
|
+
"C sources can only be compiled on Windows (needs MSVC)."
|
|
261
|
+
)
|
|
262
|
+
if not source.is_file():
|
|
263
|
+
raise LauncherBuildError(f"C source missing: {source}")
|
|
264
|
+
env = {k.upper(): v for k, v in os.environ.items()} | _vcvars_env(
|
|
265
|
+
arch, vcvars_ver=vcvars_ver, winsdk_version=winsdk_version
|
|
266
|
+
)
|
|
267
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
with tempfile.TemporaryDirectory(prefix="kivy-cc-") as tmp:
|
|
269
|
+
tmpdir = Path(tmp)
|
|
270
|
+
obj = tmpdir / (source.stem + ".obj")
|
|
271
|
+
out = tmpdir / dest.name
|
|
272
|
+
cl_exe = _which("cl", env)
|
|
273
|
+
link_exe = _which("link", env)
|
|
274
|
+
cl = [cl_exe, *_CL_FLAGS, f"/Fo{obj}", str(source)]
|
|
275
|
+
_run(cl, cwd=tmpdir, env=env, step="compile")
|
|
276
|
+
link = [
|
|
277
|
+
link_exe,
|
|
278
|
+
*_LINK_FLAGS,
|
|
279
|
+
f"/SUBSYSTEM:{subsystem}",
|
|
280
|
+
f"/OUT:{out}",
|
|
281
|
+
str(obj),
|
|
282
|
+
*_LINK_LIBS,
|
|
283
|
+
*extra_libs,
|
|
284
|
+
]
|
|
285
|
+
_run(link, cwd=tmpdir, env=env, step="link")
|
|
286
|
+
if not out.is_file():
|
|
287
|
+
raise LauncherBuildError("link produced no output binary.")
|
|
288
|
+
shutil.copy2(out, dest)
|
|
289
|
+
return dest
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def compile_launcher(
|
|
293
|
+
dest: Path,
|
|
294
|
+
*,
|
|
295
|
+
arch: str = "x64",
|
|
296
|
+
vcvars_ver: str | None = None,
|
|
297
|
+
winsdk_version: str | None = None,
|
|
298
|
+
) -> Path:
|
|
299
|
+
"""Compile ``launcher.c`` to *dest* deterministically. Returns *dest*."""
|
|
300
|
+
return compile_c_source(
|
|
301
|
+
SOURCE,
|
|
302
|
+
dest,
|
|
303
|
+
subsystem="WINDOWS",
|
|
304
|
+
arch=arch,
|
|
305
|
+
vcvars_ver=vcvars_ver,
|
|
306
|
+
winsdk_version=winsdk_version,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _which(tool: str, env: dict[str, str]) -> str:
|
|
311
|
+
"""Resolve *tool* to an absolute path using the loaded MSVC env's PATH.
|
|
312
|
+
|
|
313
|
+
Windows' ``CreateProcess`` searches the *parent* process PATH, not the
|
|
314
|
+
child ``env`` we pass to ``subprocess`` — so ``cl``/``link`` must be resolved
|
|
315
|
+
against the vcvars PATH ourselves.
|
|
316
|
+
"""
|
|
317
|
+
found = shutil.which(tool, path=env.get("PATH", ""))
|
|
318
|
+
if found is None:
|
|
319
|
+
pin = pinned_toolset()
|
|
320
|
+
hint = (
|
|
321
|
+
f" The pinned toolset {pin.label()!r} (vendor/TOOLSET.txt) may not "
|
|
322
|
+
"be installed on this machine/runner — re-vendor against a current "
|
|
323
|
+
"toolset via the `revendor_launcher` CI workflow."
|
|
324
|
+
if pin
|
|
325
|
+
else " The Visual Studio C++ build tools may be incomplete."
|
|
326
|
+
)
|
|
327
|
+
raise LauncherBuildError(f"{tool} not found on the MSVC toolset PATH.{hint}")
|
|
328
|
+
return found
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _run(cmd: list[str], *, cwd: Path, env: dict[str, str], step: str) -> None:
|
|
332
|
+
proc = subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, text=True)
|
|
333
|
+
if proc.returncode != 0:
|
|
334
|
+
raise LauncherBuildError(
|
|
335
|
+
f"launcher {step} step failed (exit {proc.returncode}):\n"
|
|
336
|
+
f"{proc.stdout}\n{proc.stderr}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def has_tool_version_stamp(image: bytes) -> bool:
|
|
341
|
+
"""True if *image* (PE bytes) still carries an MSVC "rich header".
|
|
342
|
+
|
|
343
|
+
The rich header sits between the DOS stub and the ``PE\\0\\0`` signature at
|
|
344
|
+
``e_lfanew``, and records the exact build of every tool that contributed to
|
|
345
|
+
the image — which is precisely what differs between two runner images
|
|
346
|
+
carrying the same ``VCTOOLSVERSION``. ``/EMITTOOLVERSIONINFO:NO`` omits it;
|
|
347
|
+
this is the assertion that the flag took effect, so a toolchain that
|
|
348
|
+
quietly stops honoring it fails the build instead of silently re-breaking
|
|
349
|
+
cross-machine reproducibility.
|
|
350
|
+
"""
|
|
351
|
+
if len(image) < 0x40 or image[:2] != b"MZ":
|
|
352
|
+
return False
|
|
353
|
+
e_lfanew = int.from_bytes(image[0x3C:0x40], "little")
|
|
354
|
+
if not 0x40 <= e_lfanew <= len(image):
|
|
355
|
+
return False
|
|
356
|
+
return b"Rich" in image[0x40:e_lfanew]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _reject_tool_version_stamp(image: bytes, what: str) -> None:
|
|
360
|
+
if has_tool_version_stamp(image):
|
|
361
|
+
raise LauncherBuildError(
|
|
362
|
+
f"the {what} carries an MSVC tool-version ('rich') header, so "
|
|
363
|
+
"/EMITTOOLVERSIONINFO:NO did not take effect. That header stamps "
|
|
364
|
+
"the serviced cl/link build, which differs between runner images "
|
|
365
|
+
"sharing one VCTOOLSVERSION — leaving it in makes the binary "
|
|
366
|
+
"non-reproducible across machines. Check that the linker still "
|
|
367
|
+
"supports the flag before vendoring this binary."
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def sha256_of(path: Path) -> str:
|
|
372
|
+
h = hashlib.sha256()
|
|
373
|
+
h.update(path.read_bytes())
|
|
374
|
+
return h.hexdigest()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _read_manifest() -> dict[str, str]:
|
|
378
|
+
if not MANIFEST.is_file():
|
|
379
|
+
return {}
|
|
380
|
+
entries: dict[str, str] = {}
|
|
381
|
+
for line in MANIFEST.read_text(encoding="utf-8").splitlines():
|
|
382
|
+
line = line.strip()
|
|
383
|
+
if not line or line.startswith("#"):
|
|
384
|
+
continue
|
|
385
|
+
digest, _, name = line.partition(" ")
|
|
386
|
+
if digest and name:
|
|
387
|
+
entries[name.strip()] = digest.strip()
|
|
388
|
+
return entries
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _write_manifest(entries: dict[str, str]) -> None:
|
|
392
|
+
lines = [
|
|
393
|
+
"# SHA-256 of vendored Windows binaries (kivyforge).",
|
|
394
|
+
"# Regenerate the launcher with: python -m "
|
|
395
|
+
"kivyforge.platforms.windows.launcher.build_launcher build",
|
|
396
|
+
]
|
|
397
|
+
for name in sorted(entries):
|
|
398
|
+
lines.append(f"{entries[name]} {name}")
|
|
399
|
+
MANIFEST.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def do_build() -> int:
|
|
403
|
+
pin = pinned_toolset()
|
|
404
|
+
# Resolve once, record the full MSVC + Windows SDK pair, and compile
|
|
405
|
+
# against that exact pair — never re-resolve via a major.minor / "latest
|
|
406
|
+
# SDK" alias, which would silently pick different builds on another runner.
|
|
407
|
+
env = {k.upper(): v for k, v in os.environ.items()} | _vcvars_env(
|
|
408
|
+
vcvars_ver=pin.vc_tools if pin else None,
|
|
409
|
+
winsdk_version=pin.winsdk if pin else None,
|
|
410
|
+
)
|
|
411
|
+
tools_version = _vc_tools_version(env)
|
|
412
|
+
sdk_version = _windows_sdk_version(env)
|
|
413
|
+
if not tools_version:
|
|
414
|
+
raise LauncherBuildError(
|
|
415
|
+
"vcvars did not export VCTOOLSVERSION; cannot record a "
|
|
416
|
+
"reproducible toolset pin in vendor/TOOLSET.txt."
|
|
417
|
+
)
|
|
418
|
+
recorded = ToolsetPin(tools_version, sdk_version or None)
|
|
419
|
+
write_toolset_pin(recorded)
|
|
420
|
+
compile_launcher(
|
|
421
|
+
VENDORED_LAUNCHER,
|
|
422
|
+
vcvars_ver=recorded.vc_tools,
|
|
423
|
+
winsdk_version=recorded.winsdk,
|
|
424
|
+
)
|
|
425
|
+
_reject_tool_version_stamp(VENDORED_LAUNCHER.read_bytes(), "freshly built launcher")
|
|
426
|
+
digest = sha256_of(VENDORED_LAUNCHER)
|
|
427
|
+
entries = _read_manifest()
|
|
428
|
+
entries[LAUNCHER_NAME] = digest
|
|
429
|
+
_write_manifest(entries)
|
|
430
|
+
print(f"built {VENDORED_LAUNCHER} ({digest}); {recorded.label()}")
|
|
431
|
+
return 0
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def do_verify() -> int:
|
|
435
|
+
if not VENDORED_LAUNCHER.is_file():
|
|
436
|
+
raise LauncherBuildError(
|
|
437
|
+
f"vendored launcher missing: {VENDORED_LAUNCHER}. Run the `build` "
|
|
438
|
+
"subcommand to produce it."
|
|
439
|
+
)
|
|
440
|
+
vendored = VENDORED_LAUNCHER.read_bytes()
|
|
441
|
+
pin = pinned_toolset()
|
|
442
|
+
# Resolve first so a missing/mismatched pin fails with the toolset name,
|
|
443
|
+
# and so the mismatch error can report which compiler + SDK actually ran.
|
|
444
|
+
env = {k.upper(): v for k, v in os.environ.items()} | _vcvars_env(
|
|
445
|
+
vcvars_ver=pin.vc_tools if pin else None,
|
|
446
|
+
winsdk_version=pin.winsdk if pin else None,
|
|
447
|
+
)
|
|
448
|
+
used = ToolsetPin(_vc_tools_version(env), _windows_sdk_version(env) or None)
|
|
449
|
+
with tempfile.TemporaryDirectory(prefix="kivy-launcher-verify-") as tmp:
|
|
450
|
+
rebuilt_path = Path(tmp) / LAUNCHER_NAME
|
|
451
|
+
compile_launcher(
|
|
452
|
+
rebuilt_path,
|
|
453
|
+
vcvars_ver=pin.vc_tools if pin else None,
|
|
454
|
+
winsdk_version=pin.winsdk if pin else None,
|
|
455
|
+
)
|
|
456
|
+
rebuilt = rebuilt_path.read_bytes()
|
|
457
|
+
_reject_tool_version_stamp(rebuilt, "freshly compiled launcher")
|
|
458
|
+
if rebuilt != vendored:
|
|
459
|
+
# A vendored binary that still carries the stamp predates
|
|
460
|
+
# /EMITTOOLVERSIONINFO:NO and can never match a current rebuild — say so
|
|
461
|
+
# rather than sending the reader off to compare toolset versions.
|
|
462
|
+
cause = (
|
|
463
|
+
" The vendored binary still carries an MSVC tool-version ('rich') "
|
|
464
|
+
"header, so it predates the /EMITTOOLVERSIONINFO:NO link flag and "
|
|
465
|
+
"cannot match a current rebuild. Re-vendor once via the "
|
|
466
|
+
"`revendor_launcher` CI workflow; this is expected on the commit "
|
|
467
|
+
"that introduced the flag."
|
|
468
|
+
if has_tool_version_stamp(vendored)
|
|
469
|
+
else " Either the C source or the compile/link flags changed "
|
|
470
|
+
"without re-vendoring (run `build`), or this runner's "
|
|
471
|
+
"MSVC/Windows-SDK pair emits different code than the one that "
|
|
472
|
+
"produced the vendored binary — re-vendor via the "
|
|
473
|
+
"`revendor_launcher` CI workflow when the hosted image rolls."
|
|
474
|
+
)
|
|
475
|
+
raise LauncherBuildError(
|
|
476
|
+
"the freshly compiled launcher does not byte-match the vendored "
|
|
477
|
+
f"binary ({VENDORED_LAUNCHER}).\n"
|
|
478
|
+
f" vendored: {len(vendored)} bytes, sha256={hashlib.sha256(vendored).hexdigest()}\n"
|
|
479
|
+
f" rebuilt: {len(rebuilt)} bytes, sha256={hashlib.sha256(rebuilt).hexdigest()}\n"
|
|
480
|
+
f" pinned: {pin.label() if pin else '(none)'}; "
|
|
481
|
+
f"compiler used: {used.label()}\n" + cause
|
|
482
|
+
)
|
|
483
|
+
manifest = _read_manifest()
|
|
484
|
+
expected = manifest.get(LAUNCHER_NAME)
|
|
485
|
+
actual = hashlib.sha256(vendored).hexdigest()
|
|
486
|
+
if expected and expected != actual:
|
|
487
|
+
raise LauncherBuildError(
|
|
488
|
+
f"vendored launcher SHA-256 {actual} does not match the manifest "
|
|
489
|
+
f"pin {expected}."
|
|
490
|
+
)
|
|
491
|
+
print(f"verified {VENDORED_LAUNCHER} is reproducible ({actual}); {used.label()}")
|
|
492
|
+
return 0
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def main(argv: list[str] | None = None) -> int:
|
|
496
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
497
|
+
parser.add_argument("action", choices=("build", "verify"))
|
|
498
|
+
args = parser.parse_args(argv)
|
|
499
|
+
try:
|
|
500
|
+
return do_build() if args.action == "build" else do_verify()
|
|
501
|
+
except LauncherBuildError as exc:
|
|
502
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
503
|
+
return 1
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
if __name__ == "__main__":
|
|
507
|
+
raise SystemExit(main())
|