fix: isolate Windows build dependencies
- Ограничен поиск DLL при сборке каталогами Python, окружения и Windows. - Добавлена проверка источников бинарных библиотек перед упаковкой. - Добавлен режим --verify-bundle и регрессионный smoke-тест. - Обновлены документация и версия приложения до 3.3.1.
This commit is contained in:
parent
f8f6a216f5
commit
37fbb2c545
@ -1,6 +1,8 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
@ -45,7 +47,41 @@ def git_build_ref():
|
||||
return 'untagged'
|
||||
|
||||
|
||||
def _build_library_roots():
|
||||
windows_root = Path(os.environ.get('SystemRoot', r'C:\Windows')).resolve()
|
||||
return tuple(dict.fromkeys(
|
||||
path.resolve() for path in (
|
||||
Path(sys.executable).parent,
|
||||
Path(sys.prefix),
|
||||
Path(sys.base_prefix),
|
||||
Path(sys.base_prefix) / 'DLLs',
|
||||
windows_root / 'System32',
|
||||
windows_root,
|
||||
) if path.is_dir()
|
||||
))
|
||||
|
||||
|
||||
def _set_isolated_build_path(roots):
|
||||
# Analysis searches PATH for DLLs. Unrelated tools may provide incompatible
|
||||
# copies of the Windows runtime or ICU libraries there.
|
||||
os.environ['PATH'] = os.pathsep.join(str(root) for root in roots)
|
||||
|
||||
|
||||
def _check_binary_sources(binaries, roots):
|
||||
foreign = [
|
||||
(name, source)
|
||||
for name, source, kind in binaries
|
||||
if kind == 'BINARY'
|
||||
and not any(Path(source).resolve().is_relative_to(root) for root in roots)
|
||||
]
|
||||
if foreign:
|
||||
details = '\n'.join(f'{name}: {source}' for name, source in foreign[:10])
|
||||
raise RuntimeError(f'Build contains DLLs outside Python and Windows:\n{details}')
|
||||
|
||||
|
||||
app_name = f'LEDMapper-{git_build_ref()}'
|
||||
library_roots = _build_library_roots()
|
||||
_set_isolated_build_path(library_roots)
|
||||
build_version_path = spec_dir.parent / 'build' / 'ledmapper-build-version.txt'
|
||||
build_version_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
build_version_path.write_text(app_name.removeprefix('LEDMapper-'), encoding='utf-8')
|
||||
@ -79,6 +115,7 @@ a = Analysis(
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
_check_binary_sources(a.binaries, library_roots)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
|
||||
@ -184,6 +184,8 @@ JSON схемы хранит `coordinate_unit`, `grid_step_mm` и `index_base: 0
|
||||
.\.venv\Scripts\python.exe -m PyInstaller --noconfirm --clean app\LEDMapper.spec
|
||||
```
|
||||
|
||||
Сборочный `spec` ограничивает поиск DLL каталогами Python, виртуального окружения и Windows. Это защищает EXE от несовместимых системных библиотек сторонних программ, добавленных в `PATH`. После сборки проверяйте готовый файл командой `dist\LEDMapper-vX.Y.Z.exe --verify-bundle`: успешная проверка завершается с кодом `0` без открытия окна приложения.
|
||||
|
||||
В чистой сборке из релизного тега файл будет называться:
|
||||
|
||||
```text
|
||||
|
||||
3
main.py
3
main.py
@ -26,6 +26,9 @@ except ImportError:
|
||||
|
||||
def main() -> int:
|
||||
app = QApplication(sys.argv)
|
||||
if "--verify-bundle" in sys.argv:
|
||||
app.processEvents()
|
||||
return 0
|
||||
settings = AppSettings.load()
|
||||
apply_app_theme(settings.theme)
|
||||
icon_color = "#111827" if effective_theme(settings.theme) == "light" else "#e5e7eb"
|
||||
|
||||
31
qa_smoke.py
31
qa_smoke.py
@ -264,6 +264,7 @@ def main() -> None:
|
||||
test_too_close_neighbor_has_lowest_priority,
|
||||
test_meter_like_coordinates_use_34_mm_threshold,
|
||||
test_pyinstaller_spec_build_ref_rules,
|
||||
test_pyinstaller_spec_isolates_library_paths,
|
||||
]
|
||||
for test in tests:
|
||||
gc.collect()
|
||||
@ -4299,6 +4300,36 @@ def test_pyinstaller_spec_build_ref_rules() -> None:
|
||||
assert re.fullmatch(r"\d{6}-\d{8}", dirty_ref)
|
||||
|
||||
|
||||
def test_pyinstaller_spec_isolates_library_paths() -> None:
|
||||
spec_path = PROJECT_ROOT / "app" / "LEDMapper.spec"
|
||||
spec_source = spec_path.read_text(encoding="utf-8")
|
||||
helper_source = spec_source.split("app_name = f'LEDMapper-{git_build_ref()}'", 1)[0]
|
||||
helpers = {"SPECPATH": str(spec_path.parent)}
|
||||
exec(helper_source, helpers)
|
||||
roots = helpers["_build_library_roots"]()
|
||||
assert Path(sys.prefix).resolve() in roots
|
||||
environment = helpers["os"].environ
|
||||
original_path = environment.get("PATH", "")
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
environment["PATH"] = temp_dir + helpers["os"].pathsep + original_path
|
||||
helpers["_set_isolated_build_path"](roots)
|
||||
assert temp_dir not in environment["PATH"].split(helpers["os"].pathsep)
|
||||
helpers["_check_binary_sources"](
|
||||
[("valid.dll", str(Path(sys.prefix) / "valid.dll"), "BINARY")], roots
|
||||
)
|
||||
try:
|
||||
helpers["_check_binary_sources"](
|
||||
[("foreign.dll", str(Path(temp_dir) / "foreign.dll"), "BINARY")], roots
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
assert "foreign.dll" in str(exc)
|
||||
else:
|
||||
raise AssertionError("Foreign DLL source must fail the build")
|
||||
finally:
|
||||
environment["PATH"] = original_path
|
||||
|
||||
|
||||
def path_has_segment(path, x1: float, y1: float, x2: float, y2: float) -> bool: # noqa: ANN001
|
||||
for index in range(path.elementCount() - 1):
|
||||
first = path.elementAt(index)
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
"""Текущая версия приложения; обновляется при публикации релиза."""
|
||||
|
||||
__version__ = "3.3.0"
|
||||
__version__ = "3.3.1"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user