#!/usr/bin/env python3 """ PySide6 Dependencies Analyzer with ldd support Анализирует зависимости PySide6 модулей используя ldd для определения реальных зависимостей скомпилированных библиотек. """ import ast import os import sys import subprocess import re from pathlib import Path from typing import Set, Dict, List import argparse import json class PySide6DependencyAnalyzer: def __init__(self): # Системные библиотеки, которые нужно всегда оставлять self.system_libs = { 'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl', 'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus' } self.real_dependencies = {} self.used_modules_code = set() self.used_modules_ldd = set() self.all_required_modules = set() def find_python_files(self, directory: Path) -> List[Path]: """Находит все Python файлы в директории""" python_files = [] for root, dirs, files in os.walk(directory): dirs[:] = [d for d in dirs if d not in {'.venv', '__pycache__', '.git'}] for file in files: if file.endswith('.py'): python_files.append(Path(root) / file) return python_files def find_pyside6_libs(self, base_path: Path) -> Dict[str, Path]: """Находит все PySide6 библиотеки (.so файлы)""" libs = {} # Поиск в единственной локации search_path = Path("../.venv/lib/python3.10/site-packages/PySide6") print(f"Поиск PySide6 библиотек в: {search_path}") if search_path.exists(): # Ищем .so файлы модулей for so_file in search_path.glob("Qt*.*.so"): module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore if module_name.startswith('Qt'): libs[module_name] = so_file # Также ищем в подпапках for subdir in search_path.iterdir(): if subdir.is_dir() and subdir.name.startswith('Qt'): for so_file in subdir.glob("*.so*"): if 'Qt' in so_file.name: libs[subdir.name] = so_file break return libs def analyze_ldd_dependencies(self, lib_path: Path) -> Set[str]: """Анализирует зависимости библиотеки с помощью ldd""" qt_deps = set() try: result = subprocess.run(['ldd', str(lib_path)], capture_output=True, text=True, check=True) # Парсим вывод ldd и ищем Qt библиотеки for line in result.stdout.split('\n'): # Ищем строки вида: libQt6Core.so.6 => /path/to/lib match = re.search(r'libQt6(\w+)\.so', line) if match: qt_module = f"Qt{match.group(1)}" qt_deps.add(qt_module) except (subprocess.CalledProcessError, FileNotFoundError) as e: print(f"Предупреждение: не удалось выполнить ldd для {lib_path}: {e}") return qt_deps def build_real_dependency_graph(self, pyside_libs: Dict[str, Path]) -> Dict[str, Set[str]]: """Строит граф зависимостей на основе ldd анализа""" dependencies = {} print("Анализ реальных зависимостей с помощью ldd...") for module, lib_path in pyside_libs.items(): print(f" Анализируется {module}...") deps = self.analyze_ldd_dependencies(lib_path) dependencies[module] = deps if deps: print(f" Зависимости: {', '.join(sorted(deps))}") return dependencies def analyze_file_imports(self, file_path: Path) -> Set[str]: """Анализирует один Python файл и возвращает используемые PySide6 модули""" modules = set() try: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() tree = ast.parse(content) for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: if alias.name.startswith('PySide6.'): module = alias.name.split('.', 2)[1] if module.startswith('Qt'): modules.add(module) elif isinstance(node, ast.ImportFrom): if node.module and node.module.startswith('PySide6.'): module = node.module.split('.', 2)[1] if module.startswith('Qt'): modules.add(module) except Exception as e: print(f"Ошибка при анализе {file_path}: {e}") return modules def get_all_dependencies(self, modules: Set[str], dependency_graph: Dict[str, Set[str]]) -> Set[str]: """Получает все зависимости для набора модулей, используя граф зависимостей из ldd""" all_deps = set(modules) if not dependency_graph: return all_deps # Повторяем до тех пор, пока не найдем все транзитивные зависимости changed = True iteration = 0 while changed and iteration < 10: # Защита от бесконечного цикла changed = False current_deps = set(all_deps) for module in current_deps: if module in dependency_graph: new_deps = dependency_graph[module] - all_deps if new_deps: all_deps.update(new_deps) changed = True iteration += 1 return all_deps def analyze_project(self, project_path: Path, appdir_path: Path = None) -> Dict: """Анализирует весь проект""" python_files = self.find_python_files(project_path) print(f"Найдено {len(python_files)} Python файлов") # Анализ статических импортов used_modules_code = set() file_modules = {} for file_path in python_files: modules = self.analyze_file_imports(file_path) if modules: file_modules[str(file_path.relative_to(project_path))] = list(modules) used_modules_code.update(modules) print(f"Найдено {len(used_modules_code)} модулей в коде: {', '.join(sorted(used_modules_code))}") # Поиск PySide6 библиотек search_base = appdir_path if appdir_path else project_path pyside_libs = self.find_pyside6_libs(search_base) if not pyside_libs: print("ОШИБКА: PySide6 библиотеки не найдены! Анализ невозможен.") return { 'error': 'PySide6 библиотеки не найдены', 'analysis_method': 'failed', 'found_libraries': 0, 'directly_used_code': sorted(used_modules_code), 'all_required': [], 'removable': [], 'available_modules': [], 'file_usage': file_modules } print(f"Найдено {len(pyside_libs)} PySide6 библиотек") # Анализ реальных зависимостей с ldd real_dependencies = self.build_real_dependency_graph(pyside_libs) # Определяем модули, которые реально используются через ldd used_modules_ldd = set() for module in used_modules_code: if module in real_dependencies: used_modules_ldd.update(real_dependencies[module]) used_modules_ldd.add(module) print(f"Реальные зависимости через ldd: {', '.join(sorted(used_modules_ldd))}") # Объединяем результаты анализа кода и ldd all_used_modules = used_modules_code | used_modules_ldd # Получаем все необходимые модули включая зависимости all_required = self.get_all_dependencies(all_used_modules, real_dependencies) # Все доступные PySide6 модули available_modules = set(pyside_libs.keys()) # Модули, которые можно удалить removable = available_modules - all_required return { 'analysis_method': 'ldd + static analysis', 'found_libraries': len(pyside_libs), 'directly_used_code': sorted(used_modules_code), 'directly_used_ldd': sorted(used_modules_ldd), 'all_required': sorted(all_required), 'removable': sorted(removable), 'available_modules': sorted(available_modules), 'file_usage': file_modules, 'real_dependencies': {k: sorted(v) for k, v in real_dependencies.items()}, 'library_paths': {k: str(v) for k, v in pyside_libs.items()}, 'analysis_summary': { 'total_modules': len(available_modules), 'required_modules': len(all_required), 'removable_modules': len(removable), 'space_saving_potential': f"{len(removable)/len(available_modules)*100:.1f}%" if available_modules else "0%" } } def generate_appimage_recipe(self, removable_modules: List[str], template_path: Path) -> str: """Генерирует обновленный AppImage рецепт с командами очистки""" # Читаем существующий рецепт try: with open(template_path, 'r', encoding='utf-8') as f: recipe_content = f.read() except FileNotFoundError: print(f"Шаблон рецепта не найден: {template_path}") return "" # Генерируем новые команды очистки cleanup_lines = [] # QML удаляем только если не используется qml_modules = {'QtQml', 'QtQuick', 'QtQuickWidgets'} if qml_modules.issubset(set(removable_modules)): cleanup_lines.append(" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/") # Инструменты разработки (всегда удаляем) cleanup_lines.append(" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}") # Модули для удаления if removable_modules: modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)]) cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}") # Генерируем команду для удаления нативных библиотек с сохранением нужных required_libs = set() for module in sorted(set(self.all_required_modules)): required_libs.add(f"libQt6{module.replace('Qt', '')}*") # Добавляем системные библиотеки for lib in self.system_libs: required_libs.add(f"{lib}*") keep_pattern = '|'.join(sorted(required_libs)) cleanup_lines.extend([ " - shopt -s extglob", f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})" ]) # Заменяем блок очистки в рецепте import re # Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir: pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)' new_cleanup_block = " # 5) чистим от ненужных модулей и бинарников\n" + '\n'.join(cleanup_lines) updated_recipe = re.sub(pattern, new_cleanup_block, recipe_content, flags=re.DOTALL) return updated_recipe def main(): parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd') parser.add_argument('project_path', help='Путь к проекту для анализа') parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек') parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)') parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод') args = parser.parse_args() project_path = Path(args.project_path) if not project_path.exists(): print(f"Ошибка: путь {project_path} не существует") sys.exit(1) appdir_path = Path(args.appdir) if args.appdir else None if appdir_path and not appdir_path.exists(): print(f"Предупреждение: AppDir путь {appdir_path} не существует") appdir_path = None analyzer = PySide6DependencyAnalyzer() results = analyzer.analyze_project(project_path, appdir_path) # Сохраняем в анализатор для генерации команд analyzer.all_required_modules = set(results.get('all_required', [])) # Выводим результаты print("\n" + "="*60) print("АНАЛИЗ ЗАВИСИМОСТЕЙ PYSIDE6 (ldd analysis)") print("="*60) if 'error' in results: print(f"\nОШИБКА: {results['error']}") sys.exit(1) print(f"\nМетод анализа: {results['analysis_method']}") print(f"Найдено библиотек: {results['found_libraries']}") if results['directly_used_code']: print(f"\nИспользуемые модули в коде ({len(results['directly_used_code'])}):") for module in results['directly_used_code']: print(f" • {module}") if results['directly_used_ldd']: print(f"\nРеальные зависимости через ldd ({len(results['directly_used_ldd'])}):") for module in results['directly_used_ldd']: print(f" • {module}") print(f"\nВсе необходимые модули ({len(results['all_required'])}):") for module in results['all_required']: print(f" • {module}") print(f"\nМодули, которые можно удалить ({len(results['removable'])}):") for module in results['removable']: print(f" • {module}") print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}") if args.verbose and results['real_dependencies']: Devlin(f"\nРеальные зависимости (ldd):") for module, deps in results['real_dependencies'].items(): if deps: print(f" {module} → {', '.join(deps)}") # Обновляем AppImage рецепт recipe_path = Path("../build-aux/AppImageBuilder.yml") if recipe_path.exists(): updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path) if updated_recipe: with open(recipe_path, 'w', encoding='utf-8') as f: f.write(updated_recipe) print(f"\nAppImage рецепт обновлен: {recipe_path}") else: print(f"\nОШИБКА: не удалось обновить рецепт") else: print(f"\nПредупреждение: рецепт AppImage не найден в {recipe_path}") # Сохраняем результаты в JSON if args.output: with open(args.output, 'w', encoding='utf-8') as f: json.dump(results, f, ensure_ascii=False, indent=2) print(f"Результаты сохранены в: {args.output}") print("\n" + "="*60) if __name__ == "__main__": main()