chore: debloat appimage #230
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 МиБ, символические ссылки, файлы за пределами директории темы и файлы, содержимое которых не соответствует расширению, отклоняются. Добавляйте лицензию и требуемую атрибуцию для каждого звукового ресурса, распространяемого вместе с темой.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
@@ -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:
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
|
||||
Reference in new issue
Block a user