207 lines
10 KiB
Python
207 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Проверка реальной упаковки с подставленными PE-инспектором, pacman и Inno-компилятором."""
|
|
from pathlib import Path
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
source = Path(__file__).resolve().parents[1]
|
|
root = Path(tempfile.mkdtemp(prefix='vibechat-installer-tests-'))
|
|
case = root / 'repository with spaces'
|
|
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')}
|
|
|
|
|
|
def write(path, text, executable=False):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text)
|
|
if executable:
|
|
path.chmod(0o755)
|
|
return path
|
|
|
|
|
|
def run(*arguments, ok=True, extra=None):
|
|
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:
|
|
log.write(f'{arguments!r}: exit={result.returncode}\n{result.stdout}{result.stderr}\n')
|
|
if ok:
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
else:
|
|
assert result.returncode != 0, 'Unexpected packaging success'
|
|
assert 'ERROR:' in result.stdout + result.stderr
|
|
assert not list(build.glob('installer-stage.*')), 'Staging directory leaked'
|
|
return result
|
|
|
|
|
|
def package():
|
|
run()
|
|
artifact = build / 'installer/VibeChat-1.2.3-win64-setup.exe'
|
|
return json.loads(artifact.read_text())
|
|
|
|
|
|
try:
|
|
for name in ('package_win.sh', 'compiler_runtime.sh', 'build.sh', 'installer/vibechat.iss'):
|
|
target = gui / name
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(source / name, target)
|
|
for name in ('LICENSE', 'LICENSING', 'THIRD_PARTY_NOTICES'):
|
|
write(case / name, 'fixture license\n')
|
|
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(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
|
|
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
|
|
''', executable=True)
|
|
write(commands / 'pacman', '''#!/bin/bash
|
|
set -eu
|
|
if [[ "$1" = -Qqo ]]; then echo fixture-runtime; else echo "$MSYSTEM_PREFIX/share/licenses/fixture/COPYING"; fi
|
|
''', executable=True)
|
|
compiler = write(commands / 'Inno Setup/ISCC.exe', '''#!/usr/bin/env python3
|
|
from pathlib import Path
|
|
import json, os, sys
|
|
assert os.environ['MSYS2_ARG_CONV_EXCL'] == '*'
|
|
args = sys.argv[1:]
|
|
package = Path(next(a.removeprefix('/DPackageDir=') for a in args if a.startswith('/DPackageDir=')))
|
|
version = next(a.removeprefix('/DAppVersion=') for a in args if a.startswith('/DAppVersion='))
|
|
output = Path(next(a[2:] for a in args if a.startswith('/O')))
|
|
assert Path(args[-1]).name == 'vibechat.iss'
|
|
if os.environ.get('COMPILER_FAIL'): sys.exit(2)
|
|
if os.environ.get('COMPILER_NO_OUTPUT'): sys.exit(0)
|
|
files = sorted(str(p.relative_to(package)) for p in package.rglob('*') if p.is_file())
|
|
(output / f'VibeChat-{version}-win64-setup.exe').write_text(json.dumps({'files': files, 'arguments': args}))
|
|
''', executable=True)
|
|
environment.update(ISCC=str(compiler), PATH=str(commands) + os.pathsep + os.environ['PATH'])
|
|
write(build / 'chatgui_version.txt', '1.2.3\n')
|
|
write(build / 'chatgui_qt_package.txt', 'Qt6\n')
|
|
write(build / 'vibechat.exe', 'DLL Name: Qt6Widgets.dll\nDLL Name: Qt6Network.dll\nDLL Name: librlottie.dll\n')
|
|
libraries = {
|
|
'Qt6Widgets.dll': ['Qt6Core.dll', 'Qt6Gui.dll'],
|
|
'Qt6Core.dll': ['KERNEL32.dll', 'api-ms-win-core-test.dll'],
|
|
'Qt6Gui.dll': ['helper.dll'], 'Qt6Network.dll': ['Qt6Core.dll'],
|
|
'helper.dll': ['Qt6Core.dll'], 'librlottie.dll': ['KERNEL32.dll'],
|
|
'platforms/qwindows.dll': ['platform-helper.dll'],
|
|
'imageformats/qjpeg.dll': ['jpeg-helper.dll'], 'imageformats/qwebp.dll': ['webp-helper.dll'],
|
|
'platform-helper.dll': ['KERNEL32.dll'], 'jpeg-helper.dll': ['KERNEL32.dll'], 'webp-helper.dll': ['KERNEL32.dll'],
|
|
}
|
|
for name, imports in libraries.items():
|
|
content = ''.join(f'DLL Name: {dll}\n' for dll in imports)
|
|
write(build / name, content)
|
|
if '/' in name:
|
|
runtime = prefix / 'share/qt6/plugins' / name
|
|
else:
|
|
runtime = prefix / 'bin' / name
|
|
write(runtime, content)
|
|
for name in ('vibechat.cfg', 'chatgui.cfg', 'chat_data/chats.db', 'chat_data/media/private.bin',
|
|
'chatgui.log', 'vibechat.lock', 'test_gui.exe', 'CMakeCache.txt', 'Qt5Core.dll', 'obsolete.dll'):
|
|
write(build / name, 'PRIVATE OR STALE FILE\n')
|
|
|
|
run('--check')
|
|
assert not (build / 'installer').exists(), 'Preflight created build artifacts'
|
|
result = package()
|
|
expected = {'vibechat.exe', *libraries, 'licenses/LICENSE', 'licenses/LICENSING', 'licenses/THIRD_PARTY_NOTICES',
|
|
'licenses/rlottie/COPYING', 'licenses/rlottie/licenses/COPYING.MIT', 'licenses/runtime/fixture/COPYING'}
|
|
assert set(result['files']) == expected, result['files']
|
|
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')
|
|
|
|
missing = build / 'jpeg-helper.dll'
|
|
saved = missing.read_text()
|
|
missing.unlink()
|
|
failed = run(ok=False)
|
|
assert 'qjpeg.dll requires jpeg-helper.dll' in failed.stderr
|
|
write(missing, saved)
|
|
print('PASS: missing transitive plugin dependency rejects incomplete installer')
|
|
|
|
write(build / 'helper.dll', 'wrong-architecture\n')
|
|
failed = run(ok=False)
|
|
assert 'not a Windows x64 binary' in failed.stderr
|
|
write(build / 'helper.dll', 'DLL Name: Qt6Core.dll\n')
|
|
print('PASS: wrong binary architecture is rejected')
|
|
|
|
write(build / 'Qt6Core.dll', 'DLL Name: vcruntime140.dll\n')
|
|
write(root / 'Windows/System32/vcruntime140.dll', 'developer-only redistributable\n')
|
|
failed = run(ok=False)
|
|
assert 'requires vcruntime140.dll' in failed.stderr
|
|
write(build / 'Qt6Core.dll', 'DLL Name: KERNEL32.dll\n')
|
|
print('PASS: developer-installed MSVC runtime does not masquerade as an OS dependency')
|
|
|
|
write(build / 'helper.dll', 'DLL Name: Qt5Core.dll\n')
|
|
failed = run(ok=False)
|
|
assert 'mixed Qt runtime' in failed.stderr
|
|
write(build / 'helper.dll', 'DLL Name: Qt6Core.dll\n')
|
|
print('PASS: an imported DLL from a different Qt version is rejected')
|
|
|
|
write(build / 'chatgui_version.txt', 'invalid\n')
|
|
run(ok=False)
|
|
write(build / 'chatgui_version.txt', '1.2.3\n')
|
|
previous = (build / 'installer/VibeChat-1.2.3-win64-setup.exe').read_bytes()
|
|
run(ok=False, extra={'COMPILER_FAIL': '1'})
|
|
run(ok=False, extra={'COMPILER_NO_OUTPUT': '1'})
|
|
assert (build / 'installer/VibeChat-1.2.3-win64-setup.exe').read_bytes() == previous
|
|
print('PASS: invalid version and compiler failures cannot reuse stale output as success')
|
|
|
|
run('--check', ok=False, extra={'ISCC': str(root / 'missing.exe')})
|
|
run('--check', ok=False, extra={'MSYSTEM': ''})
|
|
print('PASS: missing Inno compiler and non-Windows shell fail preflight')
|
|
|
|
# Проверяем уведомления кастомного FFmpeg вместе со статически встроенными кодеками.
|
|
original_exe = (build / 'vibechat.exe').read_text()
|
|
write(build / 'vibechat.exe', original_exe + 'DLL Name: avcodec-61.dll\n')
|
|
write(build / 'avcodec-61.dll', 'DLL Name: KERNEL32.dll\n')
|
|
write(gui / 'ffmpeg/ffmpeg_build/install/bin/avcodec-61.dll', 'DLL Name: KERNEL32.dll\n')
|
|
write(gui / 'ffmpeg/ffmpeg_build/src/ffmpeg/COPYING.GPLv3', 'FFmpeg license\n')
|
|
write(gui / 'ffmpeg/ffmpeg_build/src/x264/COPYING', 'x264 license\n')
|
|
ffmpeg = package()
|
|
assert 'avcodec-61.dll' in ffmpeg['files']
|
|
assert 'licenses/ffmpeg/ffmpeg/COPYING.GPLv3' in ffmpeg['files']
|
|
assert 'licenses/ffmpeg/x264/COPYING' in ffmpeg['files']
|
|
write(build / 'vibechat.exe', original_exe)
|
|
shutil.rmtree(gui / 'ffmpeg')
|
|
(build / 'avcodec-61.dll').unlink()
|
|
print('PASS: custom FFmpeg and its static codec license notices are included')
|
|
|
|
# Исполняем настоящий GUI build.sh: пакетирование обязано быть частью успешной Release-сборки.
|
|
write(build / 'CMakeCache.txt', 'CMAKE_GENERATOR:INTERNAL=Ninja\n')
|
|
write(commands / 'cmake', '''#!/bin/bash
|
|
set -eu
|
|
printf '%s\\n' "$*" >> "$CMAKE_CALLS"
|
|
''', executable=True)
|
|
shutil.copy(commands / 'objdump', prefix / 'bin/objdump')
|
|
write(gui / 'setup_firewall.bat', 'fixture\n')
|
|
write(gui / 'rlottie_build/librlottie.dll', 'DLL Name: KERNEL32.dll\n')
|
|
for name in ('libstdc++-6.dll', 'libgcc_s_seh-1.dll', 'libwinpthread-1.dll', 'zlib1.dll', 'libcrypto-3.dll'):
|
|
write(prefix / 'bin' / name, 'DLL Name: KERNEL32.dll\n')
|
|
original_exe += f'DLL Name: {name}\n'
|
|
write(build / 'vibechat.exe', original_exe)
|
|
calls = root / 'cmake-calls.log'
|
|
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
|
|
failed = subprocess.run(['bash', str(gui / 'build.sh'), '--installer'], cwd=gui,
|
|
env={**environment, 'CMAKE_CALLS': str(calls), 'COMPILER_FAIL': '1'}, text=True, capture_output=True)
|
|
assert failed.returncode != 0 and 'Build SUCCESS' not in failed.stdout
|
|
print('PASS: GUI installer flag selects Release and propagates packaging errors')
|
|
|
|
shutil.rmtree(root)
|
|
except Exception:
|
|
print(f'ERROR: installer fixture retained: {root}')
|
|
raise
|