refrain 0.4.2__tar.gz → 0.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {refrain-0.4.2 → refrain-0.4.3}/.github/ISSUE_TEMPLATE/bug_report.yml +1 -1
- {refrain-0.4.2 → refrain-0.4.3}/.github/workflows/security.yml +1 -1
- {refrain-0.4.2 → refrain-0.4.3}/CHANGELOG.md +28 -1
- {refrain-0.4.2 → refrain-0.4.3}/PKG-INFO +1 -1
- {refrain-0.4.2 → refrain-0.4.3}/packaging/appimage/AppImageBuilder.yml +1 -1
- {refrain-0.4.2 → refrain-0.4.3}/packaging/aur/refrain/.SRCINFO +3 -3
- {refrain-0.4.2 → refrain-0.4.3}/packaging/aur/refrain/PKGBUILD +4 -2
- {refrain-0.4.2 → refrain-0.4.3}/packaging/aur/refrain-git/.SRCINFO +1 -1
- {refrain-0.4.2 → refrain-0.4.3}/packaging/aur/refrain-git/PKGBUILD +1 -1
- {refrain-0.4.2 → refrain-0.4.3}/packaging/flatpak/io.github.Rockykln.Refrain.metainfo.xml +9 -0
- {refrain-0.4.2 → refrain-0.4.3}/packaging/flatpak/io.github.Rockykln.Refrain.yaml +2 -2
- {refrain-0.4.2 → refrain-0.4.3}/pyproject.toml +1 -1
- refrain-0.4.3/src/refrain/__init__.py +1 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/app.py +46 -10
- refrain-0.4.3/tests/test_qt_plugin_path.py +197 -0
- refrain-0.4.2/src/refrain/__init__.py +0 -1
- {refrain-0.4.2 → refrain-0.4.3}/.editorconfig +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.gitattributes +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.github/FUNDING.yml +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.github/codeql/codeql-config.yml +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.github/dependabot.yml +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.github/pull_request_template.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.github/workflows/codeql.yml +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.github/workflows/release.yml +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.github/workflows/tests.yml +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/.gitignore +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/CODE_OF_CONDUCT.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/CONTRIBUTING.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/LICENSE +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/Makefile +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/PRIVACY.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/README.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/ROADMAP.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/SECURITY.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/architecture.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/bluetooth.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/faq.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/lastfm.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/README.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/discord-rpc.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/live-log.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/notification.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/settings-advanced.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/settings-general.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/settings-sources.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/settings-updates.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/tray-menu.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/update-dialog.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/screenshots/welcome.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/docs/test-matrix.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/packaging/README.md +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/packaging/flatpak/io.github.Rockykln.Refrain.desktop +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/packaging/flatpak/python-deps.json +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/requirements-dev.txt +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/requirements.txt +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/__main__.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/github-mark.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/menu-quit.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/menu-update.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/refrain.png +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/refrain.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/tray-paused-dark.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/tray-paused.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/tray-playing-dark.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/tray-playing.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/tray-stopped-dark.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/icons/tray-stopped.svg +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/assets/refrain.desktop +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/autostart.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/config.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/cover_art.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/cover_fetcher.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/daemon.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/discord_rpc.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_de.qm +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_de.ts +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_es.qm +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_es.ts +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_fr.qm +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_fr.ts +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_it.qm +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_it.ts +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_ja.qm +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_ja.ts +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_pl.qm +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_pl.ts +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_pt.qm +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_pt.ts +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_ru.qm +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_ru.ts +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_zh_CN.qm +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/i18n/refrain_zh_CN.ts +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/logging_setup.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/paths.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/scrobble.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/scrobble_queue.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/secrets_store.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/single_instance.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/sources/__init__.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/sources/base.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/sources/bluetooth.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/sources/mpris.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/sources/mpris_server.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/timing.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/ui/__init__.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/ui/log_window.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/ui/settings_window.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/ui/tray.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/ui/update_dialog.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/ui/welcome_dialog.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/uninstall.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/src/refrain/updater.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/__init__.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/conftest.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_autostart.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_config.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_config_preserve.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_cover_art.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_cover_fetcher.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_daemon_album_format.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_daemon_idle.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_discord_listening.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_discord_rpc.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_imports.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_mpris_dispatch.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_notify_argv.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_paths.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_scrobble.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_scrobble_queue.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_scrobbler.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_secrets_store.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_settings_lastfm.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_source_selection.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_sources_base.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_timing.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_tray_init.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_uninstall.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_update_dialog_body.py +0 -0
- {refrain-0.4.2 → refrain-0.4.3}/tests/test_updater.py +0 -0
|
@@ -78,7 +78,7 @@ jobs:
|
|
|
78
78
|
fetch-depth: 0
|
|
79
79
|
|
|
80
80
|
- name: Run TruffleHog
|
|
81
|
-
uses: trufflesecurity/trufflehog@
|
|
81
|
+
uses: trufflesecurity/trufflehog@bcfcf73aaf4759d4dadc2783177c245a02792318 # v3.97.0
|
|
82
82
|
with:
|
|
83
83
|
# --only-verified: a secret only counts as flagged when TruffleHog
|
|
84
84
|
# successfully validates it against the live service. Drops the
|
|
@@ -9,6 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
Nothing here
|
|
11
11
|
|
|
12
|
+
## [0.4.3] - 2026-08-24
|
|
13
|
+
|
|
14
|
+
Fixed a startup crash on installs that run against the PySide6 wheel's
|
|
15
|
+
bundled Qt while the distro ships a newer Qt patch release.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **No Qt platform plugin could be initialized on a patch-version skew.**
|
|
20
|
+
`_augment_qt_plugin_path()` adds the distro's Qt plugin tree so a
|
|
21
|
+
pip / pipx / AppImage install can still find the system styles, and it
|
|
22
|
+
gated that on Qt's `MAJOR.MINOR` alone. That is not the rule Qt
|
|
23
|
+
enforces: Qt refuses any plugin built against a *newer* Qt than the one
|
|
24
|
+
running. On a rolling distro one patch ahead of the wheel (bundled Qt
|
|
25
|
+
6.11.1, system Qt 6.11.2) the check passed, and because
|
|
26
|
+
`addLibraryPath()` *prepends*, the rejected system `wayland` and `xcb`
|
|
27
|
+
plugins shadowed the wheel's own working copies instead of falling back
|
|
28
|
+
to them — the app aborted with "This application failed to start
|
|
29
|
+
because no Qt platform plugin could be initialized". Two guards now
|
|
30
|
+
apply: the version check mirrors Qt's actual rule (same `MAJOR.MINOR`
|
|
31
|
+
*and* the system Qt no newer than ours), and the plugin tree is
|
|
32
|
+
appended rather than prepended, so the bundled Qt keeps first claim on
|
|
33
|
+
the platform plugin. A mismatched system tree can now only cost the
|
|
34
|
+
themed style, never the ability to start. Installs running against the
|
|
35
|
+
distro PySide6 — the AUR and distro packages — were never affected;
|
|
36
|
+
they short-circuit before any of this.
|
|
37
|
+
|
|
12
38
|
## [0.4.2] - 2026-08-12
|
|
13
39
|
|
|
14
40
|
Fixed Apple Music RPC URL handling and Discord buttons,
|
|
@@ -1237,7 +1263,8 @@ with a proper, installable Linux app.
|
|
|
1237
1263
|
pip-audit, trufflehog, release), Dependabot, issue + PR templates,
|
|
1238
1264
|
`CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`.
|
|
1239
1265
|
|
|
1240
|
-
[Unreleased]: https://github.com/Rockykln/refrain/compare/v0.4.
|
|
1266
|
+
[Unreleased]: https://github.com/Rockykln/refrain/compare/v0.4.3...HEAD
|
|
1267
|
+
[0.4.3]: https://github.com/Rockykln/refrain/compare/v0.4.2...v0.4.3
|
|
1241
1268
|
[0.4.2]: https://github.com/Rockykln/refrain/compare/v0.4.1...v0.4.2
|
|
1242
1269
|
[0.4.1]: https://github.com/Rockykln/refrain/compare/v0.4.0...v0.4.1
|
|
1243
1270
|
[0.4.0]: https://github.com/Rockykln/refrain/compare/v0.3.0...v0.4.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
pkgbase = refrain
|
|
2
2
|
pkgdesc = Discord Rich Presence for Apple Music on Linux
|
|
3
|
-
pkgver = 0.4.
|
|
3
|
+
pkgver = 0.4.3
|
|
4
4
|
pkgrel = 1
|
|
5
5
|
url = https://github.com/Rockykln/refrain
|
|
6
6
|
arch = any
|
|
@@ -14,7 +14,7 @@ pkgbase = refrain
|
|
|
14
14
|
depends = python-dbus
|
|
15
15
|
depends = pyside6
|
|
16
16
|
optdepends = python-gobject: enables MPRIS-server publication so Plasma media controls reach Refrain
|
|
17
|
-
source = refrain-0.4.
|
|
18
|
-
sha256sums =
|
|
17
|
+
source = refrain-0.4.3.tar.gz::https://github.com/Rockykln/refrain/archive/v0.4.3.tar.gz
|
|
18
|
+
sha256sums = 0000000000000000000000000000000000000000000000000000000000000000
|
|
19
19
|
|
|
20
20
|
pkgname = refrain
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Maintainer: Rockykln <contact@rockykln.com>
|
|
2
2
|
|
|
3
3
|
pkgname=refrain
|
|
4
|
-
pkgver=0.4.
|
|
4
|
+
pkgver=0.4.3
|
|
5
5
|
pkgrel=1
|
|
6
6
|
pkgdesc="Discord Rich Presence for Apple Music on Linux"
|
|
7
7
|
arch=('any')
|
|
@@ -23,7 +23,9 @@ makedepends=(
|
|
|
23
23
|
'python-wheel'
|
|
24
24
|
)
|
|
25
25
|
source=("$pkgname-$pkgver.tar.gz::https://github.com/Rockykln/refrain/archive/v$pkgver.tar.gz")
|
|
26
|
-
|
|
26
|
+
# Filled in once the v0.4.3 tarball exists (after the tag is pushed):
|
|
27
|
+
# makepkg -g / updpkgsums
|
|
28
|
+
sha256sums=('0000000000000000000000000000000000000000000000000000000000000000')
|
|
27
29
|
|
|
28
30
|
build() {
|
|
29
31
|
cd "$pkgname-$pkgver"
|
|
@@ -51,6 +51,15 @@
|
|
|
51
51
|
<content_rating type="oars-1.1" />
|
|
52
52
|
|
|
53
53
|
<releases>
|
|
54
|
+
<release version="0.4.3" date="2026-08-24">
|
|
55
|
+
<description>
|
|
56
|
+
<p>
|
|
57
|
+
Fixed a startup crash on systems whose Qt is a patch release ahead
|
|
58
|
+
of the one bundled with PySide6, which left pip, pipx and AppImage
|
|
59
|
+
installs unable to initialise any Qt platform plugin.
|
|
60
|
+
</p>
|
|
61
|
+
</description>
|
|
62
|
+
</release>
|
|
54
63
|
<release version="0.4.2" date="2026-08-12">
|
|
55
64
|
<description>
|
|
56
65
|
<p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.3"
|
|
@@ -123,6 +123,40 @@ def _detect_system_qt6_version() -> str | None:
|
|
|
123
123
|
return None
|
|
124
124
|
|
|
125
125
|
|
|
126
|
+
def _qt_version_tuple(version: str) -> tuple[int, ...]:
|
|
127
|
+
"""Parse ``"6.11.2"`` into ``(6, 11, 2)``, stopping at the first
|
|
128
|
+
non-numeric component so suffixed versions still compare sanely."""
|
|
129
|
+
parts: list[int] = []
|
|
130
|
+
for chunk in version.split("."):
|
|
131
|
+
digits = ""
|
|
132
|
+
for ch in chunk:
|
|
133
|
+
if not ch.isdigit():
|
|
134
|
+
break
|
|
135
|
+
digits += ch
|
|
136
|
+
if not digits:
|
|
137
|
+
break
|
|
138
|
+
parts.append(int(digits))
|
|
139
|
+
return tuple(parts)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _system_qt_plugins_loadable(bundled: str, system: str) -> bool:
|
|
143
|
+
"""Whether the system Qt's plugins can load into our bundled Qt.
|
|
144
|
+
|
|
145
|
+
Qt refuses a plugin built against a *newer* Qt than the one running,
|
|
146
|
+
so the system tree is only usable when it shares our MAJOR.MINOR and
|
|
147
|
+
is no newer at the patch level. A distro that is a single patch ahead
|
|
148
|
+
(bundled 6.11.1, system 6.11.2 — the common case on a rolling distro)
|
|
149
|
+
would otherwise hand us platform plugins Qt then rejects.
|
|
150
|
+
"""
|
|
151
|
+
b = _qt_version_tuple(bundled)
|
|
152
|
+
s = _qt_version_tuple(system)
|
|
153
|
+
if len(b) < 2 or len(s) < 2:
|
|
154
|
+
return False
|
|
155
|
+
if b[:2] != s[:2]:
|
|
156
|
+
return False
|
|
157
|
+
return s <= b
|
|
158
|
+
|
|
159
|
+
|
|
126
160
|
# Where distros put Qt 6 plugins. Order matters: try the
|
|
127
161
|
# multiarch path first (Debian/Ubuntu derivatives), then lib64
|
|
128
162
|
# (Fedora/RHEL/openSUSE), then the plain lib (Arch/CachyOS).
|
|
@@ -151,10 +185,11 @@ def _augment_qt_plugin_path() -> None:
|
|
|
151
185
|
visibly different from the AUR / system build that runs against the
|
|
152
186
|
distro PySide6.
|
|
153
187
|
|
|
154
|
-
If a system Qt 6 plugin tree exists and
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
a
|
|
188
|
+
If a system Qt 6 plugin tree exists and its plugins can actually load
|
|
189
|
+
into our Qt (see ``_system_qt_plugins_loadable``), we *append* it so
|
|
190
|
+
Qt finds the styles. Appending matters: the bundled Qt must keep first
|
|
191
|
+
claim on the platform plugin, otherwise a system plugin that Qt then
|
|
192
|
+
refuses takes the whole startup down with it.
|
|
158
193
|
|
|
159
194
|
Must be called before ``QApplication`` is constructed — Qt resolves
|
|
160
195
|
the platform/style plugin during construction.
|
|
@@ -174,13 +209,11 @@ def _augment_qt_plugin_path() -> None:
|
|
|
174
209
|
if not system_version:
|
|
175
210
|
log.debug("Could not determine system Qt 6 version; not augmenting plugin path")
|
|
176
211
|
return
|
|
177
|
-
|
|
178
|
-
system_minor = ".".join(system_version.split(".")[:2])
|
|
179
|
-
if bundled_minor != system_minor:
|
|
212
|
+
if not _system_qt_plugins_loadable(bundled_version, system_version):
|
|
180
213
|
log.debug(
|
|
181
|
-
"Qt
|
|
182
|
-
bundled_version,
|
|
214
|
+
"System Qt %s cannot supply plugins for bundled Qt %s; not augmenting plugin path",
|
|
183
215
|
system_version,
|
|
216
|
+
bundled_version,
|
|
184
217
|
)
|
|
185
218
|
return
|
|
186
219
|
log.info(
|
|
@@ -189,7 +222,10 @@ def _augment_qt_plugin_path() -> None:
|
|
|
189
222
|
bundled_version,
|
|
190
223
|
system_version,
|
|
191
224
|
)
|
|
192
|
-
|
|
225
|
+
# Append, never prepend (``addLibraryPath`` prepends): the bundled Qt
|
|
226
|
+
# keeps first claim on the platform plugin, so a mismatched system tree
|
|
227
|
+
# can only cost us the styles, never the ability to start.
|
|
228
|
+
QCoreApplication.setLibraryPaths(QCoreApplication.libraryPaths() + [str(system_plugins)])
|
|
193
229
|
|
|
194
230
|
|
|
195
231
|
def _user_apps_dir() -> Path:
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""System Qt plugin-path augmentation must never break startup.
|
|
2
|
+
|
|
3
|
+
Regression test for the 0.4.2 pipx ship-blocker: ``_augment_qt_plugin_path``
|
|
4
|
+
compared only Qt's MAJOR.MINOR, so a PySide6 wheel bundling Qt 6.11.1 on a
|
|
5
|
+
distro carrying Qt 6.11.2 (a rolling distro one patch ahead) *prepended* the
|
|
6
|
+
system plugin tree via ``addLibraryPath``. Qt refuses a plugin built against
|
|
7
|
+
a newer Qt than the one running, so both the wayland and xcb platform
|
|
8
|
+
plugins were found, rejected, and — because the prepended path won — never
|
|
9
|
+
fell back to the wheel's own. The app aborted with "no Qt platform plugin
|
|
10
|
+
could be initialized".
|
|
11
|
+
|
|
12
|
+
Two independent guards are asserted here:
|
|
13
|
+
* the version check rejects a system Qt newer than ours, and
|
|
14
|
+
* augmentation appends rather than prepends, so even a wrongly accepted
|
|
15
|
+
system tree can only cost the styles, never the ability to start.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from types import SimpleNamespace
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
pytest.importorskip("PySide6")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Version compatibility
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
@pytest.mark.parametrize(
|
|
32
|
+
("bundled", "system", "loadable"),
|
|
33
|
+
[
|
|
34
|
+
# The shipped bug: wheel Qt 6.11.1 against Arch/CachyOS Qt 6.11.2.
|
|
35
|
+
("6.11.1", "6.11.2", False),
|
|
36
|
+
# System older or equal is fine — Qt loads older plugins.
|
|
37
|
+
("6.11.2", "6.11.1", True),
|
|
38
|
+
("6.11.2", "6.11.2", True),
|
|
39
|
+
("6.11.2", "6.11", True),
|
|
40
|
+
# Minor mismatches stay off-limits in both directions.
|
|
41
|
+
("6.11.0", "6.12.0", False),
|
|
42
|
+
("6.12.0", "6.11.0", False),
|
|
43
|
+
("6.11.0", "5.15.2", False),
|
|
44
|
+
# Unparsable input must fail closed, not raise.
|
|
45
|
+
("6.11.1", "garbage", False),
|
|
46
|
+
("", "6.11.1", False),
|
|
47
|
+
],
|
|
48
|
+
)
|
|
49
|
+
def test_system_qt_plugins_loadable(bundled, system, loadable):
|
|
50
|
+
from refrain.app import _system_qt_plugins_loadable
|
|
51
|
+
|
|
52
|
+
assert _system_qt_plugins_loadable(bundled, system) is loadable
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_qt_version_tuple_stops_at_non_numeric():
|
|
56
|
+
from refrain.app import _qt_version_tuple
|
|
57
|
+
|
|
58
|
+
assert _qt_version_tuple("6.11.2") == (6, 11, 2)
|
|
59
|
+
assert _qt_version_tuple("6.11") == (6, 11)
|
|
60
|
+
assert _qt_version_tuple("6.11.2rc1") == (6, 11, 2)
|
|
61
|
+
assert _qt_version_tuple("garbage") == ()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Augmentation behaviour
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
class _FakeQCoreApplication:
|
|
68
|
+
"""Records library-path mutations the way Qt would apply them."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, initial: list[str]) -> None:
|
|
71
|
+
self._paths = list(initial)
|
|
72
|
+
|
|
73
|
+
def libraryPaths(self) -> list[str]:
|
|
74
|
+
return list(self._paths)
|
|
75
|
+
|
|
76
|
+
def setLibraryPaths(self, paths) -> None:
|
|
77
|
+
self._paths = list(paths)
|
|
78
|
+
|
|
79
|
+
def addLibraryPath(self, path: str) -> None:
|
|
80
|
+
# Qt prepends — the behaviour that caused the crash.
|
|
81
|
+
self._paths.insert(0, str(path))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _install_fakes(monkeypatch, tmp_path, *, bundled: str, system: str):
|
|
85
|
+
"""Point ``_augment_qt_plugin_path`` at a wheel-shaped bundled tree
|
|
86
|
+
(no ``styles/``) and a fake system tree, and capture path mutations."""
|
|
87
|
+
import refrain.app as app
|
|
88
|
+
|
|
89
|
+
bundled_plugins = tmp_path / "wheel" / "PySide6" / "Qt" / "plugins"
|
|
90
|
+
(bundled_plugins / "platforms").mkdir(parents=True)
|
|
91
|
+
|
|
92
|
+
system_plugins = tmp_path / "usr" / "lib" / "qt6" / "plugins"
|
|
93
|
+
(system_plugins / "styles").mkdir(parents=True)
|
|
94
|
+
|
|
95
|
+
fake_info = SimpleNamespace(
|
|
96
|
+
LibraryPath=SimpleNamespace(PluginsPath=object()),
|
|
97
|
+
path=lambda _which: str(bundled_plugins),
|
|
98
|
+
version=lambda: SimpleNamespace(toString=lambda: bundled),
|
|
99
|
+
)
|
|
100
|
+
monkeypatch.setattr(app, "QLibraryInfo", fake_info)
|
|
101
|
+
monkeypatch.setattr(app, "_find_system_qt6_plugin_path", lambda: system_plugins)
|
|
102
|
+
monkeypatch.setattr(app, "_detect_system_qt6_version", lambda: system)
|
|
103
|
+
monkeypatch.setattr(app.sys, "platform", "linux")
|
|
104
|
+
|
|
105
|
+
fake_qapp = _FakeQCoreApplication([str(bundled_plugins)])
|
|
106
|
+
monkeypatch.setattr(app, "QCoreApplication", fake_qapp)
|
|
107
|
+
return app, fake_qapp, bundled_plugins, system_plugins
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_newer_system_qt_is_not_added(monkeypatch, tmp_path):
|
|
111
|
+
"""The exact shipped failure: system one patch ahead must be skipped."""
|
|
112
|
+
app, qapp, bundled, _system = _install_fakes(
|
|
113
|
+
monkeypatch, tmp_path, bundled="6.11.1", system="6.11.2"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
app._augment_qt_plugin_path()
|
|
117
|
+
|
|
118
|
+
assert qapp.libraryPaths() == [str(bundled)]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_compatible_system_qt_is_appended_not_prepended(monkeypatch, tmp_path):
|
|
122
|
+
"""When the versions do line up, the bundled tree keeps first claim on
|
|
123
|
+
the platform plugin — the system tree may only be consulted after it."""
|
|
124
|
+
app, qapp, bundled, system = _install_fakes(
|
|
125
|
+
monkeypatch, tmp_path, bundled="6.11.2", system="6.11.2"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
app._augment_qt_plugin_path()
|
|
129
|
+
|
|
130
|
+
assert qapp.libraryPaths() == [str(bundled), str(system)]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_distro_pyside_short_circuits(monkeypatch, tmp_path):
|
|
134
|
+
"""Running against the distro PySide6 (its plugin dir already has
|
|
135
|
+
``styles/``) must leave the library paths completely alone."""
|
|
136
|
+
import refrain.app as app
|
|
137
|
+
|
|
138
|
+
plugins = tmp_path / "usr" / "lib" / "qt6" / "plugins"
|
|
139
|
+
(plugins / "styles").mkdir(parents=True)
|
|
140
|
+
|
|
141
|
+
fake_info = SimpleNamespace(
|
|
142
|
+
LibraryPath=SimpleNamespace(PluginsPath=object()),
|
|
143
|
+
path=lambda _which: str(plugins),
|
|
144
|
+
version=lambda: SimpleNamespace(toString=lambda: "6.11.2"),
|
|
145
|
+
)
|
|
146
|
+
monkeypatch.setattr(app, "QLibraryInfo", fake_info)
|
|
147
|
+
monkeypatch.setattr(app.sys, "platform", "linux")
|
|
148
|
+
|
|
149
|
+
def _boom(): # pragma: no cover - must not be reached
|
|
150
|
+
raise AssertionError("system plugin lookup should be short-circuited")
|
|
151
|
+
|
|
152
|
+
monkeypatch.setattr(app, "_find_system_qt6_plugin_path", _boom)
|
|
153
|
+
|
|
154
|
+
fake_qapp = _FakeQCoreApplication([str(plugins)])
|
|
155
|
+
monkeypatch.setattr(app, "QCoreApplication", fake_qapp)
|
|
156
|
+
|
|
157
|
+
app._augment_qt_plugin_path()
|
|
158
|
+
|
|
159
|
+
assert fake_qapp.libraryPaths() == [str(plugins)]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_unknown_system_version_is_not_added(monkeypatch, tmp_path):
|
|
163
|
+
app, qapp, bundled, _system = _install_fakes(
|
|
164
|
+
monkeypatch, tmp_path, bundled="6.11.2", system="6.11.2"
|
|
165
|
+
)
|
|
166
|
+
monkeypatch.setattr(app, "_detect_system_qt6_version", lambda: None)
|
|
167
|
+
|
|
168
|
+
app._augment_qt_plugin_path()
|
|
169
|
+
|
|
170
|
+
assert qapp.libraryPaths() == [str(bundled)]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_non_linux_is_left_alone(monkeypatch, tmp_path):
|
|
174
|
+
app, qapp, bundled, _system = _install_fakes(
|
|
175
|
+
monkeypatch, tmp_path, bundled="6.11.2", system="6.11.2"
|
|
176
|
+
)
|
|
177
|
+
monkeypatch.setattr(app.sys, "platform", "darwin")
|
|
178
|
+
|
|
179
|
+
app._augment_qt_plugin_path()
|
|
180
|
+
|
|
181
|
+
assert qapp.libraryPaths() == [str(bundled)]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_real_augmentation_never_breaks_qapplication(monkeypatch, tmp_path):
|
|
185
|
+
"""Belt-and-braces: the real function against the real Qt must leave a
|
|
186
|
+
constructible QApplication behind."""
|
|
187
|
+
from PySide6.QtCore import QCoreApplication
|
|
188
|
+
|
|
189
|
+
import refrain.app as app
|
|
190
|
+
|
|
191
|
+
before = QCoreApplication.libraryPaths()
|
|
192
|
+
app._augment_qt_plugin_path()
|
|
193
|
+
after = QCoreApplication.libraryPaths()
|
|
194
|
+
|
|
195
|
+
# Whatever it decided, the originally-first path stays first.
|
|
196
|
+
assert after[: len(before)] == before
|
|
197
|
+
assert Path(after[0]).is_dir()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|