From 2e70c3baed3e6ab8e8b653cd2b081dc0c8830262 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 11 Oct 2026 00:21:07 +0200 Subject: [PATCH] Add VibeChat Windows installer and runtime packaging --- tools/chatgui/installer/vibechat.iss | 99 ++++++++++++++++++++ tools/chatgui/package_win.sh | 135 +++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 tools/chatgui/installer/vibechat.iss create mode 100644 tools/chatgui/package_win.sh diff --git a/tools/chatgui/installer/vibechat.iss b/tools/chatgui/installer/vibechat.iss new file mode 100644 index 00000000..3e80eb95 --- /dev/null +++ b/tools/chatgui/installer/vibechat.iss @@ -0,0 +1,99 @@ +; Параметры комплекта и версии передаёт package_win.sh, пользовательских данных в комплекте нет. +#ifndef AppVersion + #error AppVersion must be supplied by package_win.sh +#endif +#ifndef PackageDir + #error PackageDir must be supplied by package_win.sh +#endif + +[Setup] +AppId={{3A06309E-A5D4-46A6-A946-298634697A22} +AppName=VibeChat +AppVersion={#AppVersion} +AppPublisher=uTun +DefaultDirName={autopf}\VibeChat +DisableDirPage=no +DisableProgramGroupPage=yes +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +MinVersion=10.0 +PrivilegesRequired=admin +OutputBaseFilename=VibeChat-{#AppVersion}-win64-setup +Compression=lzma2 +SolidCompression=yes +WizardStyle=modern +UninstallDisplayIcon={app}\vibechat.exe +LicenseFile={#PackageDir}\licenses\LICENSE +CloseApplications=yes +CloseApplicationsFilter=*.exe,*.dll +RestartApplications=no +SetupLogging=yes +UninstallLogging=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "{#PackageDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{autoprograms}\VibeChat"; Filename: "{app}\vibechat.exe"; WorkingDir: "{app}" +Name: "{autodesktop}\VibeChat"; Filename: "{app}\vibechat.exe"; WorkingDir: "{app}"; Tasks: desktopicon + +[Run] +Filename: "{app}\vibechat.exe"; Description: "{cm:LaunchProgram,VibeChat}"; WorkingDir: "{app}"; Flags: nowait postinstall skipifsilent runasoriginaluser + +[CustomMessages] +english.FirewallFailed=Could not configure the VibeChat firewall rule. See the installation log for details. +russian.FirewallFailed=Не удалось настроить правило брандмауэра VibeChat. Подробности в журнале установки. + +[Code] +// Удаляем только правило нашего инсталлятора для конкретного установленного EXE. +procedure RemoveFirewallRule; +var + Parameters: String; + ResultCode: Integer; +begin + Parameters := 'advfirewall firewall delete rule name="VibeChat (installer)" program=' + AddQuotes(ExpandConstant('{app}\vibechat.exe')); + if not Exec(ExpandConstant('{sys}\netsh.exe'), Parameters, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + Log(Format('WARNING: cannot run firewall rule removal: error=%d', [ResultCode])) + else if ResultCode <> 0 then + Log(Format('Firewall removal: rule absent or removal failed, exit=%d', [ResultCode])) + else + Log('Firewall: previous VibeChat rule removed'); +end; + +// После копирования EXE разрешаем входящие подключения; ошибка установки видна и в журнале, и в GUI. +procedure CurStepChanged(CurStep: TSetupStep); +var + Parameters: String; + ResultCode: Integer; +begin + if CurStep <> ssPostInstall then Exit; + Log('VibeChat: installed to ' + ExpandConstant('{app}') + '; user config, keys and history are preserved'); + RemoveFirewallRule; + Parameters := 'advfirewall firewall add rule name="VibeChat (installer)" dir=in action=allow program=' + + AddQuotes(ExpandConstant('{app}\vibechat.exe')) + ' enable=yes profile=any'; + if not Exec(ExpandConstant('{sys}\netsh.exe'), Parameters, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then begin + Log(Format('ERROR: cannot run firewall rule creation: error=%d', [ResultCode])); + RaiseException(CustomMessage('FirewallFailed')); + end; + if ResultCode <> 0 then begin + Log(Format('ERROR: firewall rule creation failed: exit=%d', [ResultCode])); + RaiseException(CustomMessage('FirewallFailed')); + end; + Log('Firewall: inbound VibeChat connections allowed for installed EXE'); +end; + +// Деинсталлятор освобождает своё правило, не удаляя LocalAppData и пользовательские файлы. +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usUninstall then begin + RemoveFirewallRule; + Log('VibeChat: uninstalling program files; user config, keys and history are preserved'); + end; +end; diff --git a/tools/chatgui/package_win.sh b/tools/chatgui/package_win.sh new file mode 100644 index 00000000..b812edb2 --- /dev/null +++ b/tools/chatgui/package_win.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# Чистый комплект из Windows-сборки и Inno Setup; ключи/БД/логи из build не копируются. +set -euo pipefail + +SRC_DIR=$(cd "$(dirname "$0")" && pwd) +BUILD_DIR="$SRC_DIR/build" +RUNTIME_PREFIX="${MSYSTEM_PREFIX:-/ucrt64}" +STAGE="" +trap 'rc=$?; if [[ $rc != 0 ]]; then echo "ERROR: installer packaging failed (exit=$rc)" >&2; fi + if [[ -n "$STAGE" ]]; then rm -rf -- "$STAGE" || echo "WARN: cannot remove installer staging directory: $STAGE" >&2; fi' EXIT + +[[ "${MSYSTEM:-}" = UCRT64 ]] || { echo "ERROR: Windows installer requires MSYS2 UCRT64" >&2; exit 1; } +[[ $# = 0 || ( $# = 1 && $1 = --check ) ]] || { echo "ERROR: usage: bash package_win.sh [--check]" >&2; exit 1; } +for tool in cygpath objdump pacman; do + command -v "$tool" >/dev/null || { echo "ERROR: required packaging tool not found: $tool" >&2; exit 1; } +done + +if [[ -n "${ISCC:-}" ]]; then + ISCC=$(cygpath -u "$ISCC") +else + ISCC=$(command -v ISCC.exe || command -v iscc.exe || true) + if [[ -z "$ISCC" ]]; then + for version in 7 6; do + for variable in 'ProgramFiles(x86)' PROGRAMFILES 'ProgramFiles'; do + program_dir=$(printenv "$variable" || true) + [[ -n "$program_dir" ]] || continue + candidate=$(cygpath -u "$program_dir/Inno Setup $version/ISCC.exe") + if [[ -f "$candidate" ]]; then ISCC=$candidate; break 2; fi + done + [[ -z "$ISCC" ]] || break + done + fi +fi +[[ -n "$ISCC" && -f "$ISCC" ]] || { + echo 'ERROR: Inno Setup 6.3+ not found; install it or set ISCC to the full path of ISCC.exe' >&2 + exit 1 +} +echo "INFO: installer compiler: $ISCC" +[[ "${1:-}" != --check ]] || exit 0 + +for file in vibechat.exe chatgui_version.txt chatgui_qt_package.txt; do + [[ -f "$BUILD_DIR/$file" ]] || { echo "ERROR: missing build output: $BUILD_DIR/$file" >&2; exit 1; } +done +APP_VERSION=$(tr -d '\r\n' < "$BUILD_DIR/chatgui_version.txt") +[[ "$APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "ERROR: invalid application version: $APP_VERSION" >&2; exit 1; } +QT_PACKAGE=$(tr -d '\r\n' < "$BUILD_DIR/chatgui_qt_package.txt") +[[ "$QT_PACKAGE" = Qt5 || "$QT_PACKAGE" = Qt6 ]] || { echo "ERROR: invalid Qt package: $QT_PACKAGE" >&2; exit 1; } +for file in "${QT_PACKAGE}Core.dll" "${QT_PACKAGE}Gui.dll" "${QT_PACKAGE}Network.dll" "${QT_PACKAGE}Widgets.dll" \ + librlottie.dll platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll; do + [[ -f "$BUILD_DIR/$file" ]] || { echo "ERROR: missing runtime file: $BUILD_DIR/$file" >&2; exit 1; } +done + +STAGE=$(mktemp -d "$BUILD_DIR/installer-stage.XXXXXX") +PACKAGE_DIR="$STAGE/package" +mkdir -p "$PACKAGE_DIR/licenses" "$STAGE/output" "$BUILD_DIR/installer" +cp "$BUILD_DIR/vibechat.exe" "$PACKAGE_DIR/" +shopt -s nullglob nocasematch +for file in "$BUILD_DIR/"*.dll; do + name=${file##*/} + if [[ "$name" = Qt[56]*.dll && "$name" != "$QT_PACKAGE"*.dll ]]; then + echo "INFO: excluding DLL from other Qt version: $name" + continue + fi + cp "$file" "$PACKAGE_DIR/" +done +for file in platforms/qwindows.dll imageformats/qjpeg.dll imageformats/qwebp.dll; do + mkdir -p "$PACKAGE_DIR/${file%/*}" + cp "$BUILD_DIR/$file" "$PACKAGE_DIR/$file" +done +for file in LICENSE LICENSING THIRD_PARTY_NOTICES; do + cp "$SRC_DIR/../../$file" "$PACKAGE_DIR/licenses/" +done +mkdir -p "$PACKAGE_DIR/licenses/rlottie" +cp "$SRC_DIR/rlottie_src/COPYING" "$PACKAGE_DIR/licenses/rlottie/" +cp -R "$SRC_DIR/rlottie_src/licenses" "$PACKAGE_DIR/licenses/rlottie/" + +# Проверяем архитектуру/полноту уже собранного комплекта, включая динамические Qt-плагины. +SYSTEM_DIR=$(cygpath -u "${WINDIR:-C:\\Windows}/System32") +declare -A LICENSE_PACKAGES=() +for file in "$PACKAGE_DIR/vibechat.exe" "$PACKAGE_DIR/"*.dll "$PACKAGE_DIR/platforms/"*.dll "$PACKAGE_DIR/imageformats/"*.dll; do + name=${file##*/} + objdump -f "$file" > "$STAGE/headers.txt" + rg_output=$(awk '/file format/ {print $NF}' "$STAGE/headers.txt") + [[ "$rg_output" = pei-x86-64 ]] || { echo "ERROR: $name is not a Windows x64 binary: $rg_output" >&2; exit 1; } + objdump -p "$file" > "$STAGE/imports.txt" + while read -r dep; do + [[ -n "$dep" ]] || continue + [[ -f "$PACKAGE_DIR/$dep" ]] && continue + case "$dep" in api-ms-win-*.dll|ext-ms-win-*.dll) continue ;; esac + 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") + echo "INFO: verified installer runtime: ${file#"$PACKAGE_DIR/"}" + + # Лицензии внешних библиотек берём из пакетов той же UCRT64-сборки. + [[ "$name" != vibechat.exe && "$name" != librlottie.dll ]] || continue + [[ ! -f "$SRC_DIR/ffmpeg/ffmpeg_build/install/bin/$name" ]] || continue + runtime_file="$RUNTIME_PREFIX/bin/$name" + if [[ "$file" = "$PACKAGE_DIR/platforms/"* || "$file" = "$PACKAGE_DIR/imageformats/"* ]]; then + relative=${file#"$PACKAGE_DIR/"} + 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 +done +for pkg in "${!LICENSE_PACKAGES[@]}"; do + pacman -Qql "$pkg" > "$STAGE/package-files.txt" + 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 +if [[ -d "$SRC_DIR/ffmpeg/ffmpeg_build" ]]; then + # Архив исходников FFmpeg остаётся распакованным после build_ffmpeg.sh. + for file in "$SRC_DIR/ffmpeg/ffmpeg_build/ffmpeg-"*/COPYING* "$SRC_DIR/ffmpeg/ffmpeg_build/ffmpeg-"*/LICENSE.md; do + mkdir -p "$PACKAGE_DIR/licenses/ffmpeg" + cp "$file" "$PACKAGE_DIR/licenses/ffmpeg/" + done +fi + +echo "INFO: compiling VibeChat $APP_VERSION installer (Qt package: $QT_PACKAGE)" +# MSYS2 не должен преобразовывать ключи /D и /O; пути передаём явно в Windows-формате. +MSYS2_ARG_CONV_EXCL='*' "$ISCC" "/DAppVersion=$APP_VERSION" "/DPackageDir=$(cygpath -w "$PACKAGE_DIR")" \ + "/O$(cygpath -w "$STAGE/output")" "$(cygpath -w "$SRC_DIR/installer/vibechat.iss")" +INSTALLER_NAME="VibeChat-$APP_VERSION-win64-setup.exe" +[[ -s "$STAGE/output/$INSTALLER_NAME" ]] || { echo "ERROR: compiler did not produce $INSTALLER_NAME" >&2; exit 1; } +mv -f "$STAGE/output/$INSTALLER_NAME" "$BUILD_DIR/installer/$INSTALLER_NAME" +echo "INFO: installer created: $BUILD_DIR/installer/$INSTALLER_NAME"