chore: debloat appimage #230

Merged
Boria138 merged 1 commits from debloat-appimage into main 2026-07-21 16:57:31 +00:00
16 changed files with 114 additions and 94 deletions

No files matched your search

-17
View File
@@ -83,23 +83,6 @@ echo "---------------------------------------------------------------"
pacman-key --init
pacman -S --noconfirm archlinux-keyring
# Chaotic-AUR keys
pacman-key --recv-key 3056513887B78AEB --keyserver keyserver.ubuntu.com
pacman-key --lsign-key 3056513887B78AEB
pacman -U --noconfirm 'https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-keyring.pkg.tar.zst'
pacman -U --noconfirm 'https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-mirrorlist.pkg.tar.zst'
# Enable the multilib repository
cat << EOM >> /etc/pacman.conf
[multilib]
Include = /etc/pacman.d/mirrorlist
[chaotic-aur]
Include = /etc/pacman.d/chaotic-mirrorlist
SigLevel = Never
EOM
# Disable locale noextract
sed -i -E 's@[[:space:]]*usr/share/locale/\*@@g; s@[[:space:]]+@ @g; s@[[:space:]]+$@@' /etc/pacman.conf
+49 -1
View File
@@ -14,6 +14,7 @@ export UPINFO="gitea-releases-zsync|git.linux-gaming.ru|Linux-Gaming|PortProtonQ
export DEPLOY_OPENGL=1
export DEPLOY_SDL=1
export DEPLOY_PYTHON=1
export MAIN_BIN=portprotonqt
export OPTIMIZE_LAUNCH=1
# Adjust comp settings to bypass oom-killer
@@ -37,6 +38,10 @@ elif [ -d /usr/share/portproton ]; then
fi
# QSoundEffect only needs PCM WAV and PulseAudio. Remove the optional native
# PipeWire path and FFmpeg media backend before quick-sharun scans Qt plugins.
pacman -Rdd --noconfirm libpipewire qt6-multimedia-ffmpeg
# Deploy dependencies
# Qt libs have to be passed manually due to the app being a python script
./quick-sharun \
@@ -46,8 +51,51 @@ fi
/usr/lib/libQt6Gui.so* \
/usr/lib/libQt6Multimedia.so* \
/usr/lib/libQt6Network.so* \
/usr/lib/qt6/plugins/multimedia/libffmpegmediaplugin.so \
/usr/lib/qt6/plugins/imageformats/libqwebp.so
# The native PipeWire path rejects some IEC958 device formats. Qt's PulseAudio
# path works with both PulseAudio and pipewire-pulse on the host.
echo 'QT_AUDIO_BACKEND=pulseaudio' >> ./AppDir/.env
# DEPLOY_PYTHON copies the distro's complete Python installation. Remove
# build/development content and PySide bindings not imported by PortProtonQt.
PYTHON_DIR=$(find ./AppDir/lib -maxdepth 1 -type d -name 'python3.*' -print -quit)
PYTHON_SITE=$PYTHON_DIR/site-packages
PYSIDE_DIR=$PYTHON_SITE/PySide6
rm -rf \
"$PYTHON_DIR"/ensurepip \
"$PYTHON_DIR"/idlelib \
"$PYTHON_DIR"/pydoc_data \
"$PYTHON_DIR"/tkinter \
"$PYTHON_DIR"/turtledemo \
"$PYTHON_DIR"/venv \
"$PYTHON_SITE"/Cython \
"$PYTHON_SITE"/cython.py \
"$PYTHON_SITE"/mesonbuild \
"$PYTHON_SITE"/pygments \
"$PYTHON_SITE"/pyximport \
"$PYTHON_SITE"/setuptools \
"$PYTHON_SITE"/vapoursynth \
"$PYTHON_SITE"/wheel \
"$PYTHON_SITE"/cython-*.dist-info \
"$PYTHON_SITE"/meson-*.dist-info \
"$PYTHON_SITE"/pygments-*.dist-info \
"$PYTHON_SITE"/setuptools-*.dist-info \
"$PYTHON_SITE"/vapoursynth-*.dist-info \
"$PYTHON_SITE"/wheel-*.dist-info
find "$PYTHON_DIR" -depth -type d \( -name test -o -name tests \) \
-exec rm -rf {} +
find "$PYTHON_SITE" -type f -name '*.pyi' -delete
for module in "$PYSIDE_DIR"/Qt*.so; do
case "${module##*/}" in
QtCore.*|QtGui.*|QtMultimedia.*|QtNetwork.*|QtSvg.*|QtWidgets.*) continue ;;
esac
rm -f "$module"
done
rm -rf "$PYSIDE_DIR"/QtAsyncio
# Turn AppDir into AppImage
./quick-sharun --make-appimage
+2 -2
View File
@@ -762,9 +762,9 @@ Place UI sound effects in the theme's `sounds/` directory. File names must match
| `game_launch` | Successful game process launch |
| `gamepad_connect` | Connecting a gamepad |
For example, use `sounds/navigate.ogg` or `sounds/navigate.wav`. Supported extensions, in lookup order, are `.ogg`, `.oga`, `.wav`, `.mp3`, `.flac`, `.opus`, `.m4a`, `.aac`, and `.webm`. Availability of a codec at playback time depends on the Qt Multimedia backend installed on the system.
Use uncompressed PCM WAV files, for example `sounds/navigate.wav`. Other audio formats are not supported.
Sounds are inherited per event. A child theme can override only `navigate`; missing events are resolved through its parent chain up to `standart`. When more than one supported extension exists for the same event in one theme, the first extension in the list above is used.
Sounds are inherited per event. A child theme can override only `navigate`; missing events are resolved through its parent chain up to `standart`.
Theme validation checks the extension, file size, and format signature before a sound is loaded. Sound files larger than 20 MiB, symlinks, files outside the theme directory, and files whose content does not match their extension are rejected. Include the license and attribution required by every sound asset distributed with a theme.
+2 -2
View File
@@ -762,9 +762,9 @@ ICON_COLORS = {
| `game_launch` | Успешный запуск процесса игры |
| `gamepad_connect` | Подключение геймпада |
Например, используйте `sounds/navigate.ogg` или `sounds/navigate.wav`. Поддерживаемые расширения в порядке поиска: `.ogg`, `.oga`, `.wav`, `.mp3`, `.flac`, `.opus`, `.m4a`, `.aac` и `.webm`. Доступность кодека при воспроизведении зависит от установленного в системе backend Qt Multimedia.
Используйте несжатые PCM WAV-файлы, например `sounds/navigate.wav`. Другие аудиоформаты не поддерживаются.
Звуки наследуются отдельно для каждого события. Дочерняя тема может переопределить только `navigate`; отсутствующие события ищутся по цепочке родительских тем вплоть до `standart`. Если в одной теме для события есть несколько поддерживаемых расширений, используется первое расширение из списка выше.
Звуки наследуются отдельно для каждого события. Дочерняя тема может переопределить только `navigate`; отсутствующие события ищутся по цепочке родительских тем вплоть до `standart`.
При проверке темы проверяются расширение, размер и сигнатура формата каждого звука. Файлы размером более 20 МиБ, символические ссылки, файлы за пределами директории темы и файлы, содержимое которых не соответствует расширению, отклоняются. Добавляйте лицензию и требуемую атрибуцию для каждого звукового ресурса, распространяемого вместе с темой.
+26 -26
View File
@@ -1,54 +1,56 @@
"""Sound manager for UI feedback sounds, integrated with the theme system."""
import sys
from pathlib import Path
from PySide6.QtCore import QLoggingCategory, QUrl
from PySide6.QtCore import QUrl, qInstallMessageHandler
from portprotonqt.config import ui_config
from portprotonqt.logger import get_logger
from portprotonqt.theme_security import SUPPORTED_SOUND_EXTENSIONS, is_safe_sound_file
from portprotonqt.theme_security import is_safe_sound_file
logger = get_logger(__name__)
FFMPEG_LOG_RULES = "qt.multimedia.ffmpeg=false\nqt.multimedia.ffmpeg.*=false"
SOUND_EVENTS = frozenset({
"navigate", "click", "confirm", "back", "toggle", "open",
"keyboard_key", "tab_switch",
"game_launch", "gamepad_connect",
})
MISSING_MEDIA_BACKEND_WARNING = "No QtMultimedia backends found."
def _multimedia_message_handler(message_type: object, context: object, message: str) -> None:
if message.startswith(MISSING_MEDIA_BACKEND_WARNING):
return
print(message, file=sys.stderr)
class _SoundSlot:
"""A single player slot that pre-loads a sound file."""
def __init__(self) -> None:
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
self._audio = QAudioOutput()
self._player = QMediaPlayer()
self._player.setAudioOutput(self._audio)
self._player.setLoops(QMediaPlayer.Loops.Once)
self._player.mediaStatusChanged.connect(self._handle_media_status)
previous_handler = qInstallMessageHandler(_multimedia_message_handler)
try:
from PySide6.QtMultimedia import QSoundEffect
self._effect = QSoundEffect()
finally:
qInstallMessageHandler(previous_handler)
self._effect.setLoopCount(1)
self._loaded_event: str | None = None
def _handle_media_status(self, status: object) -> None:
if status == self._player.MediaStatus.EndOfMedia:
self._player.stop()
def play(self, event: str, url: QUrl) -> None:
if (
self._loaded_event == event
and self._player.playbackState() == self._player.PlaybackState.PlayingState
and self._effect.isPlaying()
):
return
if self._loaded_event != event:
self._player.setSource(url)
self._effect.setSource(url)
self._loaded_event = event
self._player.stop()
self._player.setPosition(0)
self._player.play()
self._effect.stop()
self._effect.play()
def reset(self) -> None:
self._player.setSource(QUrl())
self._effect.stop()
self._effect.setSource(QUrl())
self._loaded_event = None
@@ -67,7 +69,6 @@ class SoundManager:
if self._initialized:
return
self._initialized = True
QLoggingCategory.setFilterRules(FFMPEG_LOG_RULES)
self._enabled = ui_config.get_sounds_enabled()
self._sounds_dirs: list[str] = []
try:
@@ -92,10 +93,9 @@ class SoundManager:
if event not in SOUND_EVENTS:
return None
for sounds_dir in self._sounds_dirs:
for extension in SUPPORTED_SOUND_EXTENSIONS:
path = Path(sounds_dir) / f"{event}{extension}"
if path.is_file() and is_safe_sound_file(str(path)):
return path
path = Path(sounds_dir) / f"{event}.wav"
if path.is_file() and is_safe_sound_file(str(path)):
return path
return None
def _get_url(self, event: str) -> QUrl | None:
+3 -20
View File
@@ -12,9 +12,6 @@ logger = get_logger(__name__)
MAX_THEME_PY_FILE_SIZE = 512 * 1024
MAX_AST_NODES = 20000
MAX_SOUND_FILE_SIZE = 20 * 1024 * 1024
SUPPORTED_SOUND_EXTENSIONS = (
".ogg", ".oga", ".wav", ".mp3", ".flac", ".opus", ".m4a", ".aac", ".webm",
)
class ThemeSecurityChecker:
@@ -572,7 +569,7 @@ def check_theme_directory_safety(theme_dir: str, allow_absolute_imports: bool =
elif filename.endswith((".ttf", ".otf")):
if not is_safe_font_file(file_path):
return False
elif filename.lower().endswith(SUPPORTED_SOUND_EXTENSIONS):
elif filename.lower().endswith(".wav"):
if not is_safe_sound_file(file_path):
return False
@@ -617,7 +614,7 @@ def is_safe_font_file(file_path: str) -> bool:
def is_safe_sound_file(file_path: str) -> bool:
"""Check a theme sound extension, size, and file signature."""
_, ext = os.path.splitext(file_path.lower())
if ext not in SUPPORTED_SOUND_EXTENSIONS:
if ext != ".wav":
logger.error("Unsafe sound file extension for %s: %s", file_path, ext)
return False
try:
@@ -632,22 +629,8 @@ def is_safe_sound_file(file_path: str) -> bool:
return False
is_valid = False
if ext in (".ogg", ".oga", ".opus"):
is_valid = header.startswith(b"OggS")
elif ext == ".wav":
if ext == ".wav":
is_valid = header.startswith(b"RIFF") and header[8:12] == b"WAVE"
elif ext == ".flac":
is_valid = header.startswith(b"fLaC")
elif ext == ".mp3":
is_valid = header.startswith(b"ID3") or (
len(header) > 1 and header[0] == 0xff and header[1] & 0xe0 == 0xe0
)
elif ext == ".m4a":
is_valid = header[4:8] == b"ftyp"
elif ext == ".aac":
is_valid = len(header) > 1 and header[0] == 0xff and header[1] & 0xf0 == 0xf0
elif ext == ".webm":
is_valid = header.startswith(b"\x1a\x45\xdf\xa3")
if not is_valid:
logger.error("Sound file %s has invalid signature", file_path)
return is_valid
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7 -6
View File
@@ -49,15 +49,16 @@ def test_sound_manager_reuses_slot_for_same_event() -> None:
assert manager._slot_index == 1
def test_sound_slot_stops_at_end_of_media() -> None:
end_of_media = object()
def test_sound_slot_restarts_loaded_effect() -> None:
slot: Any = object.__new__(_SoundSlot)
slot._player = Mock()
slot._player.MediaStatus.EndOfMedia = end_of_media
slot._effect = Mock()
slot._effect.isPlaying.return_value = False
slot._loaded_event = "navigate"
slot._handle_media_status(end_of_media)
slot.play("navigate", Mock())
slot._player.stop.assert_called_once()
slot._effect.stop.assert_called_once()
slot._effect.play.assert_called_once()
def test_sound_manager_skips_unsafe_sound_variant(tmp_path: Path) -> None:
+25 -20
View File
@@ -376,6 +376,22 @@ class TestDirectorySafety:
(d / "styles.py").write_text("color = \"red\"\n")
assert check_theme_directory_safety(str(d))
def test_valid_wav_in_theme_dir(self, tmp_path: Path) -> None:
sounds = tmp_path / "theme" / "sounds"
sounds.mkdir(parents=True)
(sounds / "navigate.wav").write_bytes(
b"RIFF\x00\x00\x00\x00WAVE" + b"\x00" * 32
)
assert check_theme_directory_safety(str(tmp_path / "theme"))
def test_invalid_wav_in_theme_dir_is_rejected(self, tmp_path: Path) -> None:
sounds = tmp_path / "theme" / "sounds"
sounds.mkdir(parents=True)
(sounds / "navigate.wav").write_bytes(b"not audio")
assert not check_theme_directory_safety(str(tmp_path / "theme"))
# === Font safety ===
@@ -398,32 +414,21 @@ class TestFontSafety:
class TestSoundSafety:
@pytest.mark.parametrize(
("extension", "header"),
[
(".ogg", b"OggS"),
(".oga", b"OggS"),
(".opus", b"OggS"),
(".wav", b"RIFF\x00\x00\x00\x00WAVE"),
(".mp3", b"ID3"),
(".flac", b"fLaC"),
(".m4a", b"\x00\x00\x00\x10ftyp"),
(".aac", b"\xff\xf1"),
(".webm", b"\x1a\x45\xdf\xa3"),
],
)
def test_supported_signatures(
self, tmp_path: Path, extension: str, header: bytes,
) -> None:
sound = tmp_path / f"sound{extension}"
sound.write_bytes(header + b"\x00" * 32)
def test_supported_wav_signature(self, tmp_path: Path) -> None:
sound = tmp_path / "sound.wav"
sound.write_bytes(b"RIFF\x00\x00\x00\x00WAVE" + b"\x00" * 32)
assert is_safe_sound_file(str(sound))
def test_rejects_disguised_sound(self, tmp_path: Path) -> None:
sound = tmp_path / "sound.ogg"
sound = tmp_path / "sound.wav"
sound.write_bytes(b"not audio")
assert not is_safe_sound_file(str(sound))
def test_rejects_non_wav_extension(self, tmp_path: Path) -> None:
sound = tmp_path / "sound.ogg"
sound.write_bytes(b"OggS" + b"\x00" * 32)
assert not is_safe_sound_file(str(sound))
# === Image safety ===