feat(appimage): use AnyLinux Appimage to support musl-libc systems

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
2025-12-21 23:55:20 +05:00
parent 58bbff8e69
commit 3686f96231
7 changed files with 162 additions and 609 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()