feat(appimage): use AnyLinux Appimage to support musl-libc systems
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
77
build-aux/AppImage/get-dependencies.sh
Executable file
77
build-aux/AppImage/get-dependencies.sh
Executable 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
|
||||
39
build-aux/AppImage/portprotonqt-appimage.sh
Executable file
39
build-aux/AppImage/portprotonqt-appimage.sh
Executable 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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user