diff --git a/.gitea/workflows/build-nightlly.yml b/.gitea/workflows/build-nightlly.yml index def829d..4c2b518 100644 --- a/.gitea/workflows/build-nightlly.yml +++ b/.gitea/workflows/build-nightlly.yml @@ -11,40 +11,33 @@ jobs: build-appimage: name: Build AppImage runs-on: ubuntu-22.04 + container: + image: archlinux:base-devel steps: + - name: Prepare container + run: | + pacman-key --init + pacman -Sy --noconfirm archlinux-keyring + pacman -Syu --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm xorg-server-xvfb zsync + - uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - name: Install required dependencies + - name: Install appimage dependencies run: | - sudo apt update - sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools python3-build python3-venv squashfs-tools strace util-linux zsync git zstd adwaita-icon-theme - - - name: Upgrade pip toolchain - run: | - python3 -m pip install --upgrade \ - pip setuptools setuptools-scm wheel packaging build - - - name: Install appimage-builder - run: | - git clone https://github.com/Boria138/appimage-builder - cd appimage-builder - pip install . - - - name: Install uv - run: | - pip install uv + cd build-aux/AppImage + chmod +x get-dependencies.sh portprotonqt-appimage.sh + ./get-dependencies.sh --git - name: Build AppImage run: | - cd build-aux - sed -i '/app_info:/,/- exec:/ s/^\(\s*version:\s*\).*/\1"0"/' AppImageBuilder.yml - appimage-builder + cd build-aux/AppImage + ./portprotonqt-appimage.sh - name: Upload AppImage uses: https://gitea.com/actions/gitea-upload-artifact@v4 with: name: PortProtonQt-AppImage - path: build-aux/PortProtonQt*.AppImage + path: build-aux/AppImage/dist/*.AppImage build-fedora: name: Build Fedora RPM diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 0bbaaa4..23351aa 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -17,39 +17,33 @@ jobs: build-appimage: name: Build AppImage runs-on: ubuntu-22.04 + container: + image: archlinux:base-devel steps: + - name: Prepare container + run: | + pacman-key --init + pacman -Sy --noconfirm archlinux-keyring + pacman -Syu --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm xorg-server-xvfb zsync + - uses: https://gitea.com/actions/checkout@v4 - - name: Install required dependencies + - name: Install appimage dependencies run: | - sudo apt update - sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools python3-build python3-venv squashfs-tools strace util-linux zsync git zstd adwaita-icon-theme - - - name: Upgrade pip toolchain - run: | - python3 -m pip install --upgrade \ - pip setuptools setuptools-scm wheel packaging build - - - name: Install appimage-builder - run: | - git clone https://github.com/Boria138/appimage-builder - cd appimage-builder - pip install . - - - name: Install uv - run: | - pip install uv + cd build-aux/AppImage + chmod +x get-dependencies.sh portprotonqt-appimage.sh + ./get-dependencies.sh - name: Build AppImage run: | - cd build-aux - appimage-builder + cd build-aux/AppImage + ./portprotonqt-appimage.sh - name: Upload AppImage uses: https://gitea.com/actions/gitea-upload-artifact@v4 with: name: PortProtonQt-AppImage - path: build-aux/PortProtonQt*.AppImage* + path: build-aux/AppImage/dist/*.AppImage build-arch: name: Build Arch Package diff --git a/.gitea/workflows/code-build.yml b/.gitea/workflows/code-build.yml index 9d29f5d..564cdbc 100644 --- a/.gitea/workflows/code-build.yml +++ b/.gitea/workflows/code-build.yml @@ -37,7 +37,7 @@ jobs: cat changed_files.txt # Check AppImage files - if grep -q "build-aux/AppImageBuilder.yml" changed_files.txt; then + if grep -q "build-aux/AppImage/" changed_files.txt; then echo "appimage=true" >> $GITHUB_OUTPUT else echo "appimage=false" >> $GITHUB_OUTPUT @@ -62,29 +62,33 @@ jobs: runs-on: ubuntu-22.04 needs: changes if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch' + container: + image: archlinux:base-devel steps: + - name: Prepare container + run: | + pacman-key --init + pacman -S --noconfirm archlinux-keyring + pacman -Syu --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm xorg-server-xvfb zsync + - uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - name: Install required dependencies + - name: Install appimage dependencies run: | - sudo apt update - sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync zstd git - - - name: Install tools - run: | - pip3 install git+https://github.com/Boria138/appimage-builder.git - pip3 install uv + cd build-aux/AppImage + chmod +x get-dependencies.sh portprotonqt-appimage.sh + ./get-dependencies.sh - name: Build AppImage run: | - cd build-aux - appimage-builder + cd build-aux/AppImage + ./portprotonqt-appimage.sh - name: Upload AppImage uses: https://gitea.com/actions/gitea-upload-artifact@v4 with: name: PortProtonQt-AppImage - path: build-aux/PortProtonQt*.AppImage + path: build-aux/AppImage/dist/*.AppImage* build-fedora: name: Build Fedora RPM diff --git a/build-aux/AppImage/get-dependencies.sh b/build-aux/AppImage/get-dependencies.sh new file mode 100755 index 0000000..f6169e9 --- /dev/null +++ b/build-aux/AppImage/get-dependencies.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +set -eu + +# Determine if git mode is enabled based on the first argument +if [ "${1:-}" = "--git" ] || [ "${1:-}" = "-g" ]; then + GIT_MODE=true +else + GIT_MODE=false +fi + +ARCH="$(uname -m)" +PACKAGE_BUILDER="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/make-aur-package.sh" +EXTRA_PACKAGES="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/get-debloated-pkgs.sh" + +if [ "$GIT_MODE" = true ]; then + echo "Using git version of PortProtonQt..." + PPQT_PKGBUILD="https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/build-aux/PKGBUILD-git" +else + echo "Using stable version of PortProtonQt..." + PPQT_PKGBUILD="https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/build-aux/PKGBUILD" +fi + + +echo "Installing dependencies..." +echo "---------------------------------------------------------------" +pacman-key --init +pacman -Syy --needed --noconfirm archlinux-keyring +pacman -Syu --needed --noconfirm \ + cabextract \ + curl \ + perl-image-exiftool \ + pyside6 \ + python-babel \ + python-beautifulsoup4 \ + python-evdev \ + python-numpy \ + python-orjson \ + python-pillow \ + python-psutil \ + python-pyudev \ + python-rapidfuzz \ + python-requests \ + python-tqdm \ + python-websocket-client \ + unrar \ + unzip \ + qt6-svg \ + qt6-wayland \ + xdg-utils + +echo "Installing debloated packages..." +echo "---------------------------------------------------------------" +wget --retry-connrefused --tries=30 "$EXTRA_PACKAGES" -O ./get-debloated-pkgs.sh +chmod +x ./get-debloated-pkgs.sh +./get-debloated-pkgs.sh --add-common --prefer-nano + +echo "Installing AUR packages..." +echo "---------------------------------------------------------------" +wget --retry-connrefused --tries=30 "$PACKAGE_BUILDER" -O ./make-aur-package.sh +chmod +x ./make-aur-package.sh + +./make-aur-package.sh --chaotic-aur icoextract +./make-aur-package.sh --chaotic-aur python-vdf + +echo "Building PortProtonQt from PKGBUILD..." +echo "---------------------------------------------------------------" +wget --retry-connrefused --tries=30 "$PPQT_PKGBUILD" -O ./PKGBUILD +makepkg -si --noconfirm + +if [ "$GIT_MODE" = true ]; then + # For git version, we use portprotonqt-git + pacman -Q portprotonqt-git | awk '{print $2; exit}' > ~/version +else + # For stable version, we use portprotonqt + pacman -Q portprotonqt | awk '{print $2; exit}' > ~/version +fi diff --git a/build-aux/AppImage/portprotonqt-appimage.sh b/build-aux/AppImage/portprotonqt-appimage.sh new file mode 100755 index 0000000..96f7a70 --- /dev/null +++ b/build-aux/AppImage/portprotonqt-appimage.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +set -eu + +SHARUN="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/quick-sharun.sh" +ARCH="$(uname -m)" +VERSION="$(cat ~/version)" +export ARCH VERSION +export OUTPATH=./dist +export DESKTOP=/usr/share/applications/ru.linux_gaming.PortProtonQt.desktop +export ICON=/usr/share/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg +export OUTNAME=PortProtonQt-"$VERSION"-anylinux-"$ARCH".AppImage +export DEPLOY_OPENGL=1 +export DEPLOY_SYS_PYTHON=1 +export DWARFS_COMP="zstd:level=15 -S22 -B5" + +# DEPLOY ALL LIBS +wget --retry-connrefused --tries=30 "$SHARUN" -O ./quick-sharun +chmod +x ./quick-sharun + +# Add udev rules +mkdir -p ./AppDir/etc/udev/rules.d +cp /usr/lib/udev/rules.d/60-portprotonqt.rules ./AppDir/etc/udev/rules.d + +# Deploy Qt translations +mkdir -p ./AppDir/usr/share/qt6/translations +cp -r /usr/share/qt6/translations/* ./AppDir/usr/share/qt6/translations/ + +# Deploy dependencies +# Qt libs have to be passed manually due to the app being a python script +./quick-sharun \ + /usr/bin/portprotonqt* \ + /usr/lib/libQt6Core.so* \ + /usr/lib/libQt6Gui.so* \ + /usr/lib/libQt6Network.so* \ + /usr/lib/libudev.so* + +# Turn AppDir into AppImage +./quick-sharun --make-appimage diff --git a/build-aux/AppImageBuilder.yml b/build-aux/AppImageBuilder.yml deleted file mode 100644 index a9954cc..0000000 --- a/build-aux/AppImageBuilder.yml +++ /dev/null @@ -1,80 +0,0 @@ -version: 1 -script: - - rm -rf AppDir || true - - mkdir -p AppDir/usr/local/lib/python3.10/dist-packages - - uv venv - - uv pip install --no-cache-dir ../ - - cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages - - cp -r share AppDir/usr - - cp -r lib AppDir/usr - - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/ - - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate} - - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*} - - shopt -s extglob - - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Network*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*) -AppDir: - path: ./AppDir - after_bundle: - - rm -rf $TARGET_APPDIR/usr/share/man || true - - rm -rf $TARGET_APPDIR/usr/share/doc || true - - rm -rf $TARGET_APPDIR/usr/share/doc-base || true - - rm -rf $TARGET_APPDIR/usr/share/info || true - - rm -rf $TARGET_APPDIR/usr/share/help || true - - rm -rf $TARGET_APPDIR/usr/share/gtk-doc || true - - rm -rf $TARGET_APPDIR/usr/share/devhelp || true - - rm -rf $TARGET_APPDIR/usr/share/examples || true - - rm -rf $TARGET_APPDIR/usr/share/pkgconfig || true - - rm -rf $TARGET_APPDIR/usr/share/bash-completion || true - - rm -rf $TARGET_APPDIR/usr/share/pixmaps || true - - rm -rf $TARGET_APPDIR/usr/share/mime || true - - rm -rf $TARGET_APPDIR/usr/share/metainfo || true - - rm -rf $TARGET_APPDIR/usr/include || true - - rm -rf $TARGET_APPDIR/usr/lib/pkgconfig || true - - find $TARGET_APPDIR -type f \( -name '*.a' -o -name '*.la' -o -name '*.h' -o -name '*.cmake' -o -name '*.pdb' \) -delete || true - - "find $TARGET_APPDIR -type f -executable -exec file {} \\; | grep ELF | grep -v '/dist-packages/' | grep -v '/site-packages/' | cut -d: -f1 | xargs strip --strip-unneeded || true" - - find $TARGET_APPDIR -type d -empty -delete || true - app_info: - id: ru.linux_gaming.PortProtonQt - name: PortProtonQt - icon: ru.linux_gaming.PortProtonQt - version: 0.1.9 - exec: usr/bin/python3 - exec_args: "-m portprotonqt.app $@" - apt: - arch: amd64 - sources: - - sourceline: 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse' - key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c' - include: - - python3-minimal - - python3-pkg-resources - - libopengl0 - - libk5crypto3 - - libkrb5-3 - - libgssapi-krb5-2 - - libxcb-cursor0 - - libimage-exiftool-perl - - xdg-utils - - cabextract - - curl - - 7zip - - unzip - - unrar - exclude: - - "*-doc" - - "*-man" - - manpages - - mandb - - "*-dev" - - "*-static" - - "*-dbg" - - "*-dbgsym" - runtime: - env: - PYTHONHOME: '${APPDIR}/usr' - PYTHONPATH: '${APPDIR}/usr/local/lib/python3.10/dist-packages' - PERLLIB: '${APPDIR}/usr/share/perl5:${APPDIR}/usr/lib/x86_64-linux-gnu/perl/5.34:${APPDIR}/usr/share/perl/5.34' -AppImage: - sign-key: None - arch: x86_64 - comp: zstd diff --git a/dev-scripts/appimage_clean.py b/dev-scripts/appimage_clean.py deleted file mode 100755 index 83bfdac..0000000 --- a/dev-scripts/appimage_clean.py +++ /dev/null @@ -1,474 +0,0 @@ -#!/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, project_root: Path = None): - # Системные библиотеки, которые нужно всегда оставлять - self.system_libs = { - 'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl', - 'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus', - 'libQt6Svg' - } - - self.critical_modules = { - 'QtSvg', - } - - self.real_dependencies = {} - self.used_modules_code = set() - self.used_modules_ldd = set() - self.all_required_modules = set() - # Определяем корень проекта - if project_root is None: - # Корень проекта - две директории выше от скрипта - self.project_root = Path(__file__).parent.parent - else: - self.project_root = project_root - - self.venv_path = self.project_root / ".venv" - self.build_path = self.project_root / "build-aux" - - 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 = {} - - # Ищем venv в корне проекта - venv_candidates = [ - self.venv_path, # .venv - self.project_root / "venv", - self.project_root / ".virtualenv", - ] - - pyside6_path = None - - # Пробуем найти PySide6 в venv - for venv in venv_candidates: - if venv.exists(): - # Ищем Python версию - lib_path = venv / "lib" - if lib_path.exists(): - for python_dir in lib_path.iterdir(): - if python_dir.name.startswith('python'): - candidate = python_dir / "site-packages" / "PySide6" - if candidate.exists(): - pyside6_path = candidate - print(f"Найден PySide6 в: {candidate}") - break - if pyside6_path: - break - - if not pyside6_path: - print(f"Предупреждение: PySide6 не найден в venv, проверяем AppDir...") - # Если не нашли в venv, пробуем в AppDir - if base_path: - appdir_candidate = base_path / "AppDir/usr/local/lib" - if appdir_candidate.exists(): - for python_dir in appdir_candidate.iterdir(): - if python_dir.name.startswith('python'): - candidate = python_dir / "dist-packages" / "PySide6" - if candidate.exists(): - pyside6_path = candidate - print(f"Найден PySide6 в AppDir: {candidate}") - break - - if not pyside6_path: - return libs - - # Ищем .so файлы модулей - for so_file in pyside6_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 pyside6_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: - removable_filtered = [m for m in removable_modules if m not in self.critical_modules] - if removable_filtered: - modules_list = ','.join([f"{mod}*" for mod in sorted(removable_filtered)]) - 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 - - # Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:) - # Паттерн: после " - cp -r lib AppDir/usr\n" идут команды rm, а затем "AppDir:" - pattern = r'( - cp -r lib AppDir/usr\n)((?: - (?:rm|shopt).*\n)*?)(?=AppDir:)' - - match = re.search(pattern, recipe_content) - - if not match: - print("ПРЕДУПРЕЖДЕНИЕ: Не удалось найти блок очистки в рецепте") - print("Добавляем команды очистки перед блоком AppDir:") - - # Просто вставим команды перед AppDir: - appdir_pos = recipe_content.find('AppDir:') - if appdir_pos != -1: - new_content = ( - recipe_content[:appdir_pos] + - '\n'.join(cleanup_lines) + '\n' + - recipe_content[appdir_pos:] - ) - return new_content - else: - print("ОШИБКА: Не найден блок AppDir: в рецепте") - return "" - - # Создаем замену - группа 1 (cp -r lib) + новые команды очистки - replacement = r'\1' + '\n'.join(cleanup_lines) + '\n' - - updated_recipe = re.sub(pattern, replacement, recipe_content, count=1) - - return updated_recipe - -def main(): - parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd') - parser.add_argument('project_path', nargs='?', default='.', - help='Путь к проекту для анализа (по умолчанию: текущая директория)') - parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек') - parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)') - parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод') - parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)') - - args = parser.parse_args() - - project_path = Path(args.project_path).resolve() - if not project_path.exists(): - print(f"Ошибка: путь {project_path} не существует") - sys.exit(1) - - appdir_path = Path(args.appdir).resolve() if args.appdir else None - if appdir_path and not appdir_path.exists(): - print(f"Предупреждение: AppDir путь {appdir_path} не существует") - appdir_path = None - - # Определяем корень проекта - # Если запущен из подпапки проекта, ищем корень - project_root = project_path - if (project_path / ".git").exists() or (project_path / "pyproject.toml").exists(): - project_root = project_path - else: - # Пытаемся найти корень проекта - current = project_path - while current != current.parent: - if (current / ".git").exists() or (current / "pyproject.toml").exists(): - project_root = current - break - current = current.parent - - print(f"Корень проекта: {project_root}") - - analyzer = PySide6DependencyAnalyzer(project_root=project_root) - - # Если указан custom venv путь - if args.venv: - analyzer.venv_path = Path(args.venv).resolve() - print(f"Использую указанный venv: {analyzer.venv_path}") - - 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']: - print(f"\nРеальные зависимости (ldd):") - for module, deps in results['real_dependencies'].items(): - if deps: - print(f" {module} → {', '.join(deps)}") - - # Обновляем AppImage рецепт - recipe_path = analyzer.build_path / "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()