From 050b2f6d052f161f89ed31e30b60cfa310cc394d Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 30 Oct 2026 11:44:06 +0100 Subject: [PATCH] Batch Windows installer staging and license queries --- tools/chatgui/package_win.sh | 56 +++++++++++-------- tools/chatgui/tests/test_windows_installer.py | 55 ++++++++++++++---- 2 files changed, 79 insertions(+), 32 deletions(-) diff --git a/tools/chatgui/package_win.sh b/tools/chatgui/package_win.sh index dfed34d37..a4b196e18 100644 --- a/tools/chatgui/package_win.sh +++ b/tools/chatgui/package_win.sh @@ -78,6 +78,7 @@ IMPORT_DUMP="$STAGE/imports.txt" VERIFY_STARTED=$SECONDS declare -A LICENSE_PACKAGES=() declare -A INCLUDED=() +PACKAGE_FILES=() PACKAGE_SOURCES=() PACKAGE_STAMPS=() LICENSE_SOURCES=() FILES=(vibechat.exe wintun.dll platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll) FILES+=("${CAPTURE_RUNTIME[@]}") HAS_FFMPEG=0 @@ -93,16 +94,9 @@ for ((i = 0; i < ${#FILES[@]}; i++)); do # Ключ кеша относится к build, а не к новой временной копии установщика. runtime_inspect "$BUILD_DIR/$relative" [[ "$binary_format" = pei-x86-64 ]] || { echo "ERROR: $name is not a Windows x64 binary: $binary_format" >&2; exit 1; } - file="$PACKAGE_DIR/$relative" - mkdir -p "${file%/*}" - cp "$BUILD_DIR/$relative" "$file" - # Успешный cp переносит проверенные байты; источник не должен меняться во время анализа/копирования. - copied_stamp=$(stat -Lc '%n|%s|%y|%z' -- "$BUILD_DIR/$relative") || { - echo "ERROR: cannot stat installer source: $relative" >&2; exit 1; - } - [[ "$copied_stamp" = "$binary_stamp" ]] || { - echo "ERROR: installer source changed during packaging: $relative; rebuild and retry" >&2; exit 1; - } + PACKAGE_FILES+=("$relative") + PACKAGE_SOURCES+=("$BUILD_DIR/$relative") + PACKAGE_STAMPS+=("$binary_stamp") while read -r dep; do [[ -n "$dep" ]] || continue if [[ -f "$BUILD_DIR/$dep" ]]; then FILES+=("$dep"); continue; fi @@ -111,8 +105,6 @@ for ((i = 0; i < ${#FILES[@]}; i++)); do echo "ERROR: incomplete installer runtime: $name requires $dep" >&2 exit 1 done <<< "$import_list" - echo "INFO: verified installer runtime: ${file#"$PACKAGE_DIR/"}" - # Лицензии внешних библиотек берём из пакетов той же UCRT64-сборки. [[ "$name" != vibechat.exe && "$name" != librlottie.dll && "$name" != wintun.dll ]] || continue if [[ -f "$SRC_DIR/ffmpeg/ffmpeg_build/install/bin/$name" ]] && ! chatgui_is_compiler_runtime_dll "$name"; then @@ -120,27 +112,47 @@ for ((i = 0; i < ${#FILES[@]}; i++)); do continue fi runtime_file="$RUNTIME_PREFIX/bin/$name" - if [[ "$file" = "$PACKAGE_DIR/platforms/"* || "$file" = "$PACKAGE_DIR/imageformats/"* || "$file" = "$PACKAGE_DIR/multimedia/"* ]]; then - relative=${file#"$PACKAGE_DIR/"} + if [[ "$relative" = platforms/* || "$relative" = imageformats/* || "$relative" = multimedia/* ]]; then runtime_file="$RUNTIME_PREFIX/share/qt${QT_PACKAGE#Qt}/plugins/$relative" [[ -f "$runtime_file" ]] || runtime_file="$RUNTIME_PREFIX/lib/qt${QT_PACKAGE#Qt}/plugins/$relative" fi [[ -f "$runtime_file" ]] || { echo "ERROR: cannot identify runtime source for $name" >&2; exit 1; } - pkg=$(pacman -Qqo "$runtime_file") - [[ -n "$pkg" ]] || { echo "ERROR: no package owns runtime file: $runtime_file" >&2; exit 1; } - LICENSE_PACKAGES["$pkg"]=1 + LICENSE_SOURCES+=("$runtime_file") done -echo "INFO: installer PE verification: scanned=$RUNTIME_SCANNED cached=$RUNTIME_HITS elapsed=$((SECONDS - VERIFY_STARTED))s" -for pkg in "${!LICENSE_PACKAGES[@]}"; do - pacman -Qql "$pkg" > "$STAGE/package-files.txt" +# Один cp вместо отдельных mkdir/cp на каждую DLL; --parents сохраняет каталоги Qt-плагинов. +(cd "$BUILD_DIR" && cp --parents -- "${PACKAGE_FILES[@]}" "$PACKAGE_DIR/") +# Проверяем одним stat, что все источники остались теми же после анализа и копирования. +copied_stamps=$(stat -Lc '%n|%s|%y|%z' -- "${PACKAGE_SOURCES[@]}") || { + echo "ERROR: cannot stat installer sources after copying" >&2; exit 1; +} +mapfile -t COPIED_STAMPS <<< "$copied_stamps" +for ((i = 0; i < ${#PACKAGE_FILES[@]}; i++)); do + [[ "${COPIED_STAMPS[i]:-}" = "${PACKAGE_STAMPS[i]}" ]] || { + echo "ERROR: installer source changed during packaging: ${PACKAGE_FILES[i]}; rebuild and retry" >&2; exit 1; + } +done +echo "INFO: installer runtime staged: files=${#PACKAGE_FILES[@]} scanned=$RUNTIME_SCANNED cached=$RUNTIME_HITS elapsed=$((SECONDS - VERIFY_STARTED))s" + +# На MSYS2 запуск процессов дорог: все владельцы файлов и списки лицензий запрашиваются пакетно. +if (( ${#LICENSE_SOURCES[@]} )); then + pacman -Qqo -- "${LICENSE_SOURCES[@]}" > "$STAGE/license-packages.txt" || { + echo "ERROR: cannot identify packages owning installer runtime files" >&2; exit 1; + } + while IFS= read -r pkg; do + [[ -z "$pkg" ]] || LICENSE_PACKAGES["$pkg"]=1 + done < "$STAGE/license-packages.txt" + (( ${#LICENSE_PACKAGES[@]} )) || { echo "ERROR: no package owns installer runtime files" >&2; exit 1; } + pacman -Qql -- "${!LICENSE_PACKAGES[@]}" > "$STAGE/package-files.txt" || { + echo "ERROR: cannot list installer runtime license files" >&2; exit 1; + } while IFS= read -r file; do [[ "$file" = */share/licenses/* && -f "$file" ]] || continue relative=${file#*/share/licenses/} mkdir -p "$PACKAGE_DIR/licenses/runtime/${relative%/*}" cp "$file" "$PACKAGE_DIR/licenses/runtime/$relative" done < "$STAGE/package-files.txt" - echo "INFO: included runtime license notices: $pkg" -done + echo "INFO: included runtime license notices: packages=${#LICENSE_PACKAGES[@]}" +fi if [[ "$HAS_FFMPEG" = 1 ]]; then FFMPEG_SOURCE="$SRC_DIR/ffmpeg/ffmpeg_build/src" [[ -f "$FFMPEG_SOURCE/ffmpeg/COPYING.GPLv3" ]] || { echo "ERROR: FFmpeg source license notices missing: $FFMPEG_SOURCE" >&2; exit 1; } diff --git a/tools/chatgui/tests/test_windows_installer.py b/tools/chatgui/tests/test_windows_installer.py index 97619b255..b41557301 100644 --- a/tools/chatgui/tests/test_windows_installer.py +++ b/tools/chatgui/tests/test_windows_installer.py @@ -15,8 +15,10 @@ build = gui / 'build' commands = root / 'commands' prefix = root / 'ucrt64' scan_log = root / 'objdump.log' +copy_log = root / 'runtime-copy.log' +pacman_log = root / 'pacman.log' environment = {**os.environ, 'MSYSTEM': 'UCRT64', 'MSYSTEM_PREFIX': str(prefix), 'WINDIR': str(root / 'Windows'), - 'OBJDUMP_CALLS': str(scan_log)} + 'OBJDUMP_CALLS': str(scan_log), 'RUNTIME_COPY_CALLS': str(copy_log), 'PACMAN_CALLS': str(pacman_log)} def write(path, text, executable=False): @@ -29,6 +31,8 @@ def write(path, text, executable=False): def run(*arguments, ok=True, extra=None): scan_log.write_text('') + copy_log.write_text('') + pacman_log.write_text('') result = subprocess.run(['bash', str(gui / 'package_win.sh'), *arguments], env={**environment, **(extra or {})}, text=True, capture_output=True) with (root / 'checks.log').open('a') as log: @@ -43,7 +47,11 @@ def run(*arguments, ok=True, extra=None): def package(): - run() + result = run() + assert 'verified installer runtime:' not in result.stdout, 'Per-DLL verification spam returned' + assert result.stdout.count('INFO: installer runtime staged:') == 1 + assert copy_log.read_text().splitlines() == ['runtime batch'], 'Expected one runtime copy process' + assert pacman_log.read_text().splitlines() == ['-Qqo', '-Qql'], 'Expected two batched package queries' artifact = build / 'installer/VibeChat-1.2.3-win64-setup.exe' return json.loads(artifact.read_text()) @@ -61,6 +69,7 @@ try: write(gui / 'rlottie_src/COPYING', 'rlottie notice\n') write(gui / 'rlottie_src/licenses/COPYING.MIT', 'MIT notice\n') write(prefix / 'share/licenses/fixture/COPYING', 'runtime notice\n') + write(prefix / 'share/licenses/helper/COPYING', 'helper notice\n') write(root / 'Windows/System32/KERNEL32.dll', 'system file\n') write(commands / 'cygpath', '#!/bin/bash\nprintf "%s\\n" "$2"\n', executable=True) write(commands / 'objdump', '''#!/bin/bash @@ -74,7 +83,30 @@ cat "$2" shutil.copy(commands / 'objdump', prefix / 'bin/objdump') write(commands / 'pacman', '''#!/bin/bash set -eu -if [[ "$1" = -Qqo ]]; then echo fixture-runtime; else echo "$MSYSTEM_PREFIX/share/licenses/fixture/COPYING"; fi +operation=$1; shift +[[ "$1" = -- ]]; shift +printf '%s\\n' "$operation" >> "$PACMAN_CALLS" +[[ ${PACMAN_FAIL:-} != "$operation" ]] || exit 1 +for target in "$@"; do + if [[ "$operation" = -Qqo ]]; then + [[ -f "$target" ]] + if [[ "$target" = */helper.dll ]]; then echo fixture-helper; else echo fixture-runtime; fi + else + case "$target" in + fixture-runtime) echo "$MSYSTEM_PREFIX/share/licenses/fixture/COPYING" ;; + fixture-helper) echo "$MSYSTEM_PREFIX/share/licenses/helper/COPYING" ;; + *) exit 1 ;; + esac + fi +done +''', executable=True) + write(commands / 'cp', '''#!/bin/bash +set -eu +/bin/cp "$@" +if [[ "$1" = --parents ]]; then + printf 'runtime batch\\n' >> "$RUNTIME_COPY_CALLS" + if [[ -n ${MUTATE_COPY_SOURCE:-} ]]; then printf '\\nchanged during copy\\n' >> "$MUTATE_COPY_SOURCE"; fi +fi ''', executable=True) compiler = write(commands / 'Inno Setup/ISCC.exe', '''#!/usr/bin/env python3 from pathlib import Path @@ -121,7 +153,8 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f result = package() expected = {'vibechat.exe', *libraries, 'licenses/LICENSE', 'licenses/LICENSING', 'licenses/THIRD_PARTY_NOTICES', 'licenses/wintun/wintun-LICENSE.txt', 'licenses/wintun/wintun-api-LICENSE.txt', - 'licenses/rlottie/COPYING', 'licenses/rlottie/licenses/COPYING.MIT', 'licenses/runtime/fixture/COPYING'} + 'licenses/rlottie/COPYING', 'licenses/rlottie/licenses/COPYING.MIT', + 'licenses/runtime/fixture/COPYING', 'licenses/runtime/helper/COPYING'} assert set(result['files']) == expected, result['files'] assert len(scan_log.read_text().splitlines()) == len(libraries) + 1, 'Expected one PE inspection per packaged binary' print('PASS: complete dependency graph, Qt plugin helpers and license notices; private files and stale DLLs excluded') @@ -130,6 +163,12 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f assert result2['files'] == result['files'] assert not scan_log.read_text(), 'Repeated packaging inspected unchanged PE files' print('PASS: repeated packaging reuses architecture and imports with no objdump calls; paths containing spaces') + print('PASS: runtime copied in one batch, license owners/listing queried in two processes, one runtime summary') + + for operation in ('-Qqo', '-Qql'): + failed = run(ok=False, extra={'PACMAN_FAIL': operation}) + assert 'ERROR: cannot ' in failed.stderr + print('PASS: failed batched package queries reject incomplete license collection') write(build / 'vibechat.exe', (build / 'vibechat.exe').read_text() + '\nrelinked\n') assert package()['files'] == result['files'] @@ -137,11 +176,6 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f print('PASS: standalone packaging inspects only a changed EXE') # Проверенные импорты относятся к конкретным байтам: изменение источника при копировании — ошибка. - write(commands / 'cp', '''#!/bin/bash -set -eu -/bin/cp "$@" -if [[ ${MUTATE_COPY_SOURCE:-} = "$1" ]]; then printf '\\nchanged during copy\\n' >> "$1"; fi -''', executable=True) source_before = (build / 'helper.dll').read_text() failed = run(ok=False, extra={'MUTATE_COPY_SOURCE': str(build / 'helper.dll')}) assert 'installer source changed during packaging: helper.dll' in failed.stderr @@ -265,7 +299,8 @@ if [[ ${FFMPEG_FAIL:-0} = 1 ]]; then exit 1; fi assert built.returncode == 0, built.stdout + built.stderr assert '-DCMAKE_BUILD_TYPE=Release' in calls.read_text() assert 'installer created:' in built.stdout and 'Build SUCCESS' in built.stdout - assert 'installer PE verification: scanned=0 cached=' in built.stdout, built.stdout + assert 'installer runtime staged:' in built.stdout and 'scanned=0 cached=' in built.stdout, built.stdout + assert 'verified installer runtime:' not in built.stdout scans = scan_log.read_text().splitlines() assert len(scans) == len(set(scans)), 'Packaging repeated a PE inspection performed by deployment' assert (build / 'wintun.dll').read_bytes() == (case / 'lib/wintun.dll').read_bytes()