diff --git a/tools/chatgui/package_win.sh b/tools/chatgui/package_win.sh index 228ccd8c..dfed34d3 100644 --- a/tools/chatgui/package_win.sh +++ b/tools/chatgui/package_win.sh @@ -2,6 +2,8 @@ # Чистый комплект из Windows-сборки и Inno Setup; ключи/БД/логи из build не копируются. set -euo pipefail +log() { echo "$1"; } + SRC_DIR=$(cd "$(dirname "$0")" && pwd) BUILD_DIR="$SRC_DIR/build" RUNTIME_PREFIX="${MSYSTEM_PREFIX:-/ucrt64}" @@ -70,6 +72,10 @@ cp "$SRC_DIR/../../lib/wintun-LICENSE.txt" "$SRC_DIR/../../lib/wintun-api-LICENS # Проверяем архитектуру/полноту уже собранного комплекта, включая динамические Qt-плагины. SYSTEM_DIR=$(cygpath -u "${WINDIR:-C:\\Windows}/System32") source "$SRC_DIR/compiler_runtime.sh" +source "$SRC_DIR/runtime_cache.sh" +runtime_cache_init +IMPORT_DUMP="$STAGE/imports.txt" +VERIFY_STARTED=$SECONDS declare -A LICENSE_PACKAGES=() declare -A INCLUDED=() FILES=(vibechat.exe wintun.dll platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll) @@ -84,14 +90,19 @@ for ((i = 0; i < ${#FILES[@]}; i++)); do echo "ERROR: mixed Qt runtime: $relative does not belong to $QT_PACKAGE" >&2 exit 1 fi + # Ключ кеша относится к 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" - name=${file##*/} - objdump -f "$file" > "$STAGE/headers.txt" - binary_format=$(awk '/file format/ {print $NF}' "$STAGE/headers.txt") - [[ "$binary_format" = pei-x86-64 ]] || { echo "ERROR: $name is not a Windows x64 binary: $binary_format" >&2; exit 1; } - objdump -p "$file" > "$STAGE/imports.txt" + # Успешный 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; + } while read -r dep; do [[ -n "$dep" ]] || continue if [[ -f "$BUILD_DIR/$dep" ]]; then FILES+=("$dep"); continue; fi @@ -99,7 +110,7 @@ for ((i = 0; i < ${#FILES[@]}; i++)); do if [[ -f "$SYSTEM_DIR/$dep" && "$dep" != msvcp*.dll && "$dep" != vcruntime*.dll ]]; then continue; fi echo "ERROR: incomplete installer runtime: $name requires $dep" >&2 exit 1 - done < <(awk '/DLL Name:/ {print $3}' "$STAGE/imports.txt") + done <<< "$import_list" echo "INFO: verified installer runtime: ${file#"$PACKAGE_DIR/"}" # Лицензии внешних библиотек берём из пакетов той же UCRT64-сборки. @@ -119,6 +130,7 @@ for ((i = 0; i < ${#FILES[@]}; i++)); do [[ -n "$pkg" ]] || { echo "ERROR: no package owns runtime file: $runtime_file" >&2; exit 1; } LICENSE_PACKAGES["$pkg"]=1 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" while IFS= read -r file; do diff --git a/tools/chatgui/tests/test_runtime_deploy.sh b/tools/chatgui/tests/test_runtime_deploy.sh index 3f978ae0..69ee3a56 100644 --- a/tools/chatgui/tests/test_runtime_deploy.sh +++ b/tools/chatgui/tests/test_runtime_deploy.sh @@ -27,6 +27,7 @@ cat > "$fixture_root/commands/objdump" <<'SH' #!/bin/bash printf '%s\n' "$2" >> "$DEPLOY_OBJDUMP_LOG" if [[ "$(< "$2")" = *BROKEN* ]]; then echo 'invalid PE fixture' >&2; exit 7; fi +printf '%s: file format pei-x86-64\n' "$2" cat "$2" SH chmod +x "$fixture_root/commands/"* @@ -35,7 +36,7 @@ prepare_case() { case_dir="$fixture_root/project/Qt$1 package"; prefix="$case_dir/ucrt64" mkdir -p "$case_dir/rlottie_build" "$prefix/bin" "$prefix/share/qt$1/plugins/platforms" "$prefix/share/qt$1/plugins/imageformats" cp "$fixture_root/commands/objdump" "$prefix/bin/objdump" - cp "$source_dir/build.sh" "$source_dir/compiler_runtime.sh" "$source_dir/runtime_cache.sh" "$case_dir/" + cp "$source_dir/build.sh" "$source_dir/package_win.sh" "$source_dir/compiler_runtime.sh" "$source_dir/runtime_cache.sh" "$case_dir/" printf 'DLL Name: KERNEL32.dll\n' > "$case_dir/rlottie_build/librlottie.dll" printf 'fixture\n' > "$case_dir/setup_firewall.bat" printf 'DLL Name: AUTHZ.dll\n' > "$prefix/bin/Qt$1Core.dll" diff --git a/tools/chatgui/tests/test_windows_installer.py b/tools/chatgui/tests/test_windows_installer.py index 3c85d13b..97619b25 100644 --- a/tools/chatgui/tests/test_windows_installer.py +++ b/tools/chatgui/tests/test_windows_installer.py @@ -14,7 +14,9 @@ gui = case / 'tools/chatgui' build = gui / 'build' commands = root / 'commands' prefix = root / 'ucrt64' -environment = {**os.environ, 'MSYSTEM': 'UCRT64', 'MSYSTEM_PREFIX': str(prefix), 'WINDIR': str(root / 'Windows')} +scan_log = root / 'objdump.log' +environment = {**os.environ, 'MSYSTEM': 'UCRT64', 'MSYSTEM_PREFIX': str(prefix), 'WINDIR': str(root / 'Windows'), + 'OBJDUMP_CALLS': str(scan_log)} def write(path, text, executable=False): @@ -26,6 +28,7 @@ def write(path, text, executable=False): def run(*arguments, ok=True, extra=None): + scan_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: @@ -46,7 +49,7 @@ def package(): try: - for name in ('package_win.sh', 'compiler_runtime.sh', 'build.sh', 'installer/vibechat.iss'): + for name in ('package_win.sh', 'compiler_runtime.sh', 'runtime_cache.sh', 'build.sh', 'installer/vibechat.iss'): target = gui / name target.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(source / name, target) @@ -62,12 +65,13 @@ try: write(commands / 'cygpath', '#!/bin/bash\nprintf "%s\\n" "$2"\n', executable=True) write(commands / 'objdump', '''#!/bin/bash set -eu -if [[ "$1" = -f ]]; then - if [[ $(head -1 "$2") = wrong-architecture ]]; then echo 'file format pei-i386'; else echo 'file format pei-x86-64'; fi -else - cat "$2" -fi +[[ "$1" = -p ]] || { echo 'unexpected extra PE inspection' >&2; exit 1; } +printf '%s\\n' "$2" >> "$OBJDUMP_CALLS" +if [[ $(head -1 "$2") = wrong-architecture ]]; then echo 'file format pei-i386'; else echo 'file format pei-x86-64'; fi +cat "$2" ''', executable=True) + (prefix / 'bin').mkdir(parents=True) + 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 @@ -119,11 +123,30 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f 'licenses/wintun/wintun-LICENSE.txt', 'licenses/wintun/wintun-api-LICENSE.txt', 'licenses/rlottie/COPYING', 'licenses/rlottie/licenses/COPYING.MIT', 'licenses/runtime/fixture/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') result2 = package() assert result2['files'] == result['files'] - print('PASS: repeated packaging and paths containing spaces') + 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') + + write(build / 'vibechat.exe', (build / 'vibechat.exe').read_text() + '\nrelinked\n') + assert package()['files'] == result['files'] + assert scan_log.read_text().splitlines() == [str(build / 'vibechat.exe')] + 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 + write(build / 'helper.dll', source_before) + print('PASS: source mutation during staging rejects the installer') driver = build / 'wintun.dll' saved_driver = driver.read_text() @@ -133,6 +156,9 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f write(driver, 'wrong-architecture\n') failed = run(ok=False) assert 'wintun.dll is not a Windows x64 binary' in failed.stderr + failed = run(ok=False) + assert 'wintun.dll is not a Windows x64 binary' in failed.stderr + assert not scan_log.read_text(), 'Cached architecture rejection required another inspection' write(driver, saved_driver) print('PASS: dynamically loaded Wintun and its licenses are included; missing/wrong architecture DLL is rejected') @@ -158,8 +184,9 @@ files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_f missing.unlink() failed = run(ok=False) assert 'qjpeg.dll requires jpeg-helper.dll' in failed.stderr + assert str(build / 'imageformats/qjpeg.dll') not in scan_log.read_text().splitlines() write(missing, saved) - print('PASS: missing transitive plugin dependency rejects incomplete installer') + print('PASS: cached plugin imports still reject a missing transitive dependency') write(build / 'helper.dll', 'wrong-architecture\n') failed = run(ok=False) @@ -232,13 +259,18 @@ if [[ ${FFMPEG_FAIL:-0} = 1 ]]; then exit 1; fi ''') environment['FFMPEG_CALLS'] = str(ffmpeg_calls) (build / 'wintun.dll').unlink() + scan_log.write_text('') built = subprocess.run(['bash', str(gui / 'build.sh'), '--installer', '-j2'], cwd=gui, env={**environment, 'CMAKE_CALLS': str(calls)}, text=True, capture_output=True) 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 + 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() assert ffmpeg_calls.read_text().splitlines() == ['profile update'] + print('PASS: build --installer shares cached PE format/imports without rescanning DLLs in staging') cmake_before_failure = calls.read_text() codec_failed = subprocess.run(['bash', str(gui / 'build.sh'), '--installer', '-j2'], cwd=gui, env={**environment, 'CMAKE_CALLS': str(calls), 'FFMPEG_FAIL': '1'}, text=True, capture_output=True)