diff --git a/winehelper_gui.py b/winehelper_gui.py
index c01851a..2e56df6 100644
--- a/winehelper_gui.py
+++ b/winehelper_gui.py
@@ -28,6 +28,396 @@ class Var:
LICENSE_FILE = os.environ.get("LICENSE_FILE")
LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT")
+class DependencyManager:
+ """Класс для управления проверкой и установкой системных зависимостей."""
+
+ def __init__(self):
+ """Инициализирует менеджер, определяя необходимые пути."""
+ self.dependencies_script_path = self._get_dependencies_path()
+ self.config_dir = os.path.join(os.path.expanduser("~"), ".config", "winehelper")
+ self.hash_flag_file = os.path.join(self.config_dir, "dependencies_hash.txt")
+ os.makedirs(self.config_dir, exist_ok=True)
+ self.app_icon = QIcon(Var.WH_ICON_PATH) if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH) else QIcon()
+
+ def _get_dependencies_path(self):
+ """Определяет и возвращает путь к скрипту dependencies.sh."""
+ winehelper_script_path = os.environ.get("RUN_SCRIPT")
+ if winehelper_script_path and os.path.exists(winehelper_script_path):
+ return os.path.join(os.path.dirname(winehelper_script_path), 'dependencies.sh')
+ return None
+
+ def _calculate_file_hash(self):
+ """Вычисляет хэш SHA256 файла зависимостей."""
+ if not self.dependencies_script_path or not os.path.exists(self.dependencies_script_path):
+ return None
+ hasher = hashlib.sha256()
+ try:
+ with open(self.dependencies_script_path, 'rb') as f:
+ while chunk := f.read(4096):
+ hasher.update(chunk)
+ return hasher.hexdigest()
+ except IOError:
+ return None
+
+ def _parse_dependencies_from_script(self):
+ """
+ Парсит скрипт dependencies.sh для извлечения списка базовых пакетов.
+ Возвращает список пакетов или None в случае ошибки.
+ """
+ if not os.path.exists(self.dependencies_script_path):
+ return None
+
+ base_packages = []
+ try:
+ with open(self.dependencies_script_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ content = content.replace('\\\n', '')
+ pattern = r'apt-get install\s+\{i586-,\}(\{.*?\}|[\w.-]+)'
+ matches = re.findall(pattern, content)
+
+ for match in matches:
+ match = match.strip()
+ if match.startswith('{') and match.endswith('}'):
+ group_content = match[1:-1]
+ packages = [pkg.strip() for pkg in group_content.split(',')]
+ base_packages.extend(packages)
+ else:
+ base_packages.append(match)
+
+ return sorted(list(set(pkg for pkg in base_packages if pkg))) or None
+ except Exception:
+ return None
+
+ def _parse_repo_error_from_script(self):
+ """
+ Парсит скрипт dependencies.sh для извлечения сообщения об ошибке репозитория.
+ Возвращает сообщение или None в случае ошибки.
+ """
+ if not os.path.exists(self.dependencies_script_path):
+ return None
+
+ try:
+ with open(self.dependencies_script_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ content = content.replace('\\\n', ' ')
+ match = re.search(r'apt-repo.*\|\|.*fatal\s+"([^"]+)"', content)
+ if match:
+ error_message = match.group(1).strip()
+ return re.sub(r'\s+', ' ', error_message)
+ return None
+ except Exception:
+ return None
+
+ def _show_startup_messages(self):
+ """
+ Проверяет, является ли это первым запуском или обновлением,
+ и показывает соответствующие сообщения.
+ """
+ current_hash = self._calculate_file_hash()
+ stored_hash = None
+ hash_file_exists = os.path.exists(self.hash_flag_file)
+
+ if hash_file_exists:
+ try:
+ with open(self.hash_flag_file, 'r', encoding='utf-8') as f:
+ stored_hash = f.read().strip()
+ except IOError:
+ pass
+
+ if not hash_file_exists:
+ msg_box = QMessageBox(QMessageBox.Information, "Первый запуск WineHelper",
+ "Поскольку это первый запуск, программа проверит наличие необходимых системных зависимостей.")
+ msg_box.setWindowIcon(self.app_icon)
+ msg_box.exec_()
+ elif current_hash != stored_hash:
+ msg_box = QMessageBox(QMessageBox.Information, "Обновление зависимостей",
+ "Обнаружены изменения в системных требованиях.\n\n"
+ "Программа выполнит проверку, чтобы убедиться, что все компоненты на месте.")
+ msg_box.setWindowIcon(self.app_icon)
+ msg_box.exec_()
+
+ def _save_dependency_hash(self):
+ """Сохраняет хэш зависимостей в конфигурационный файл."""
+ current_hash = self._calculate_file_hash()
+ if not current_hash:
+ return
+ try:
+ with open(self.hash_flag_file, 'w', encoding='utf-8') as f:
+ f.write(current_hash)
+ except IOError:
+ print("Предупреждение: не удалось записать файл с хэшем зависимостей.")
+
+ def _perform_check_and_install(self):
+ """
+ Выполняет основную логику проверки и установки зависимостей.
+ Возвращает True в случае успеха, иначе False.
+ """
+ # Проверка наличия ключевых утилит
+ if not shutil.which('apt-repo') or not shutil.which('rpm'):
+ return True
+
+ if not self.dependencies_script_path or not os.path.exists(self.dependencies_script_path):
+ msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
+ f"Файл зависимостей не найден по пути:\n'{self.dependencies_script_path}'.\n\n"
+ "Программа не может продолжить работу."
+ )
+ msg_box.setWindowIcon(self.app_icon)
+ msg_box.exec_()
+ return False
+
+ # 1. Проверка наличия репозитория x86_64-i586
+ try:
+ result = subprocess.run(['apt-repo'], capture_output=True, text=True, check=False, encoding='utf-8')
+ if result.returncode != 0 or 'x86_64-i586' not in result.stdout:
+ error_message = self._parse_repo_error_from_script()
+ if not error_message:
+ msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
+ f"Репозиторий x86_64-i586 не подключен, но не удалось извлечь текст ошибки из файла:\n'{self.dependencies_script_path}'.\n\n"
+ "Проверьте целостность скрипта. Работа программы будет прекращена."
+ )
+ msg_box.setWindowIcon(self.app_icon)
+ msg_box.exec_()
+ return False
+
+ msg_box = QMessageBox(QMessageBox.Critical, "Ошибка репозитория",
+ f"{error_message}\n\nРабота программы будет прекращена.")
+ msg_box.setWindowIcon(self.app_icon)
+ msg_box.exec_()
+ return False
+ except FileNotFoundError:
+ return True
+
+ # 2. Определение списка пакетов из dependencies.sh
+ base_packages = self._parse_dependencies_from_script()
+
+ # Если парсинг не удался или скрипт не найден, прерываем работу.
+ if not base_packages:
+ msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
+ f"Не удалось найти или проанализировать файл зависимостей:\n'{self.dependencies_script_path}'.\n\n"
+ "Программа не может продолжить работу без списка необходимых пакетов."
+ )
+ msg_box.setWindowIcon(self.app_icon)
+ msg_box.exec_()
+ return False
+
+ required_packages = [f"i586-{pkg}" for pkg in base_packages] + base_packages
+
+ # 3. Проверка, какие пакеты отсутствуют
+ try:
+ # Запрашиваем список всех установленных пакетов один раз для эффективности
+ result = subprocess.run(
+ ['rpm', '-qa', '--queryformat', '%{NAME}\n'],
+ capture_output=True, text=True, check=True, encoding='utf-8'
+ )
+ installed_packages_set = set(result.stdout.splitlines())
+ required_packages_set = set(required_packages)
+
+ # Находим разницу между требуемыми и установленными пакетами
+ missing_packages = sorted(list(required_packages_set - installed_packages_set))
+
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
+ # В случае ошибки (например, rpm не найден), показываем критическое сообщение
+ msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
+ f"Не удалось получить список установленных пакетов с помощью rpm.\n\nОшибка: {e}\n\n"
+ "Программа не может проверить зависимости и будет закрыта."
+ )
+ msg_box.setWindowIcon(self.app_icon)
+ msg_box.exec_()
+ return False
+
+ if not missing_packages:
+ return True
+
+ # 4. Запрос у пользователя на установку
+ msg_box = QMessageBox()
+ msg_box.setWindowIcon(self.app_icon)
+ msg_box.setIcon(QMessageBox.Warning)
+ msg_box.setWindowTitle("Отсутствуют зависимости")
+ # Устанавливаем формат текста в RichText, чтобы QMessageBox корректно обрабатывал HTML-теги.
+ msg_box.setTextFormat(Qt.RichText)
+
+ # Формируем весь текст как единый HTML-блок для корректного отображения.
+ full_html_text = (
+ "Для корректной работы WineHelper требуются дополнительные системные компоненты.
"
+ "Отсутствуют следующие пакеты:
"
+ # Ограничиваем высоту блока с пакетами и добавляем прокрутку, если список длинный.
+ f"""
Установка зависимостей еще не завершена.
" + "Вы действительно хотите прервать процесс?
" + "Это закроет только окно программы.
"
+ "Процесс установки зависимостей будет продолжен.
" + ) + + yes_button = msg_box.addButton("Да, прервать", QMessageBox.YesRole) + no_button = msg_box.addButton("Нет", QMessageBox.NoRole) + msg_box.setDefaultButton(no_button) + + msg_box.exec_() + + if msg_box.clickedButton() == yes_button: + process.readyRead.disconnect() + process.finished.disconnect() + process.terminate() + event.accept() + else: + event.ignore() + else: + event.accept() + + process.readyRead.connect(handle_output) + process.finished.connect(handle_finish) + + log_output.append(f"Выполнение команды:\npkexec sh -c \"{install_cmd_str}\"\n") + log_output.append("Пожалуйста, введите пароль в появившемся окне аутентификации...") + log_output.append("-" * 40 + "\n") + + dialog.closeEvent = dialog_close_handler + process.start('pkexec', ['sh', '-c', install_cmd_str]) + dialog.exec_() + + return installation_successful + + def run(self): + """ + Основной публичный метод для запуска полной проверки зависимостей. + Возвращает True, если все зависимости удовлетворены, иначе False. + """ + self._show_startup_messages() + if self._perform_check_and_install(): + self._save_dependency_hash() + return True + return False + class WinetricksManagerDialog(QDialog): """Диалог для управления компонентами Winetricks.""" @@ -542,7 +932,7 @@ class WineHelperGUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("WineHelper") - self.setGeometry(100, 100, 950, 500) + self.setMinimumSize(950, 500) if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH): self.setWindowIcon(QIcon(Var.WH_ICON_PATH)) @@ -2258,17 +2648,23 @@ class WineHelperGUI(QMainWindow): if hasattr(self, 'install_process') and self.install_process: if self.install_process.state() == QProcess.Running: self.install_process.terminate() - # Даем процессу 3 секунды на завершение if not self.install_process.waitForFinished(3000): self.append_log("Процесс не ответил на terminate, отправляется kill...", is_error=True) self.install_process.kill() - self.install_process.waitForFinished() # Ждем завершения после kill + self.install_process.waitForFinished() self.install_process.deleteLater() self.install_process = None +def main(): + """Основная точка входа в приложение.""" + app = QApplication(sys.argv) + + dependency_manager = DependencyManager() + if dependency_manager.run(): + window = WineHelperGUI() + window.show() + return app.exec_() + return 1 if __name__ == "__main__": - app = QApplication(sys.argv) - window = WineHelperGUI() - window.show() - sys.exit(app.exec_()) + sys.exit(main())