mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-06 21:44:16 +02:00
- Native runtime under bearhub/ (api, commons, gems, view/util, view/core, view/qt) - bauh/ compatibility re-export shims for legacy imports - Phase D: canonical __version__/__app_name__ in bearhub/__init__.py; pyproject.toml and setup.py point at bearhub package - Native gem loader at bearhub/view/core/gems.py - Namespace compatibility tests (159 tests passing) - Documentation: CHANGELOG [Unreleased], NAMESPACE_MIGRATION, ROADMAP, README, CONTRIBUTING, docs/qt6-migration.md, packaging/aur/README.md
251 lines
9.5 KiB
Markdown
251 lines
9.5 KiB
Markdown
# Qt6 Migration Guide (PR-6.1)
|
|
|
|
This document defines the technical approach for migrating Bearhub from **PyQt5** to a **Qt6 Python binding**.
|
|
|
|
Related roadmap milestone: `ROADMAP.md` → **M6: Qt6 Migration**.
|
|
|
|
## Goals
|
|
|
|
- Keep Bearhub feature parity during migration (no UI redesign).
|
|
- Improve long-term desktop compatibility (Wayland, HiDPI, Arch packaging).
|
|
- Remove PyQt5-only workarounds where Qt6 provides stable behavior.
|
|
- Land migration in small, reversible PR slices.
|
|
|
|
## Non-Goals (for M6)
|
|
|
|
- Rewriting the UI toolkit (GTK, Electron, web UI).
|
|
- Visual redesign or UX overhaul.
|
|
- Parallel feature development inside migration PRs.
|
|
|
|
## Prerequisites
|
|
|
|
- **M3** Phase B is complete: UI modules live under stable `bearhub.*` import paths (`bearhub/view/qt/*`, etc.). Wait for an M3 stable tag before starting PR-6.2.
|
|
- **M2** thread/uninstall crash paths are documented and reproducible.
|
|
- Unit test CI is green (`tests/`, GitHub workflow `tests.yml`).
|
|
|
|
## Current Qt Footprint (inventory)
|
|
|
|
Runtime PyQt5 usage is concentrated in:
|
|
|
|
| Area | Files | Notes |
|
|
|---|---|---|
|
|
| App entry | `bearhub/app_main.py`, `bearhub/context.py`, `bearhub/manage.py`, `bearhub/tray.py` | app attributes, screen scale env |
|
|
| Main window/UI | `bearhub/view/qt/window.py`, `apps_table.py`, `components.py`, `dialog.py` | largest migration surface |
|
|
| Async/threading | `bearhub/view/qt/thread.py`, `prepare.py`, `systray.py`, `settings.py`, `root.py` | `QThread`, `pyqtSignal` |
|
|
| Utility/UI helpers | `bearhub/view/qt/qt_utils.py`, `about.py`, `info.py`, `history.py`, `screenshots.py` | includes `QDesktopWidget` |
|
|
| Settings integration | `bearhub/view/core/settings.py` | `QStyleFactory` |
|
|
| Misc | `bearhub/view/util/util.py` | icon helpers |
|
|
|
|
Known Qt5-specific patterns in code today:
|
|
|
|
- `app.exec_()` and dialog `exec_()` calls
|
|
- `pyqtSignal` declarations
|
|
- `Qt.AA_EnableHighDpiScaling` / `Qt.AA_UseHighDpiPixmaps`
|
|
- `QDesktopWidget` screen geometry logic (`qt_utils.py`)
|
|
- Forced Wayland workaround in desktop files: `QT_QPA_PLATFORM=xcb`
|
|
|
|
Packaging/runtime dependencies today:
|
|
|
|
- Python package: `pyqt5>=5.12` (`requirements.txt`, `pyproject.toml`)
|
|
- AUR: `python-pyqt5`
|
|
- AppImage builder pins `pyqt5==5.15.10` in `linux_dist/appimage/AppImageBuilder.yml`
|
|
|
|
## Binding Decision Matrix: PySide6 vs PyQt6
|
|
|
|
| Criterion | PySide6 | PyQt6 | Notes for Bearhub |
|
|
|---|---|---|---|
|
|
| Qt version | Qt6 | Qt6 | Equal target platform |
|
|
| License | LGPL v3 | GPL v3 / commercial | Bearhub app license is `zlib/libpng` |
|
|
| License fit for permissive app | Strong | Weaker unless commercial license purchased | PySide6 is lower compliance risk for distro packaging |
|
|
| Arch packaging | `pyside6` | `python-pyqt6` | Arch `[extra]` provides `pyside6` |
|
|
| API style | Qt official style (`Signal`, `Slot`) | PyQt style (`pyqtSignal`, `pyqtSlot`) | Mechanical rename/refactor needed either way |
|
|
| Documentation/examples | Qt docs + Qt for Python | Riverbank docs | Comparable quality |
|
|
| Migration effort from PyQt5 | Medium | Medium-low | PyQt6 is closer syntactically, but not enough to outweigh license fit |
|
|
| Long-term maintenance | Qt Company backed | Riverbank | Both active |
|
|
| Wayland/tray maturity | Qt6 baseline | Qt6 baseline | Main gain comes from Qt6 itself |
|
|
|
|
### Recommendation
|
|
|
|
**Default choice: PySide6**.
|
|
|
|
Reasoning:
|
|
|
|
1. Bearhub is permissively licensed (`zlib/libpng`); LGPL binding is a safer default for distribution.
|
|
2. Migration effort is dominated by Qt5→Qt6 API changes, not PyQt5→PyQt6 naming alone.
|
|
3. Wayland/HiDPI improvements come from Qt6 runtime regardless of binding.
|
|
|
|
### Decision fallback
|
|
|
|
Choose **PyQt6** only if maintainers explicitly accept GPL obligations for combined distribution and/or purchase commercial licensing.
|
|
|
|
Record the final decision in this file before PR-6.2 starts:
|
|
|
|
```
|
|
Decision: PySide6
|
|
Date: 2026-06-26
|
|
Owner: Bearhub maintainers
|
|
Rationale: LGPL fit for zlib-licensed app; Qt6 gains independent of binding choice
|
|
```
|
|
|
|
## Migration Design
|
|
|
|
### 1) Introduce a thin Qt compatibility layer (short-lived)
|
|
|
|
Add `bearhub/view/qt/bindings.py` (or similar) exposing:
|
|
|
|
- signal/slot aliases
|
|
- app exec helper
|
|
- screen geometry helper
|
|
- common enum imports
|
|
|
|
Purpose: reduce churn while migrating module-by-module. Remove this layer in PR-6.7.
|
|
|
|
### 2) Mechanical API updates
|
|
|
|
| PyQt5 / Qt5 pattern | Qt6 target |
|
|
|---|---|
|
|
| `exec_()` | `exec()` |
|
|
| `pyqtSignal` | `Signal` (PySide6) / `pyqtSignal` (PyQt6) |
|
|
| `AA_EnableHighDpiScaling`, `AA_UseHighDpiPixmaps` | Qt6 high-DPI env defaults + `QT_SCALE_FACTOR` |
|
|
| `QDesktopWidget` | `QScreen` via `QApplication.primaryScreen()` / `screenAt()` |
|
|
| enum usage (`Qt.AlignCenter`) | namespaced enums (`Qt.AlignmentFlag.AlignCenter`) where required |
|
|
|
|
### 3) High-risk functional areas
|
|
|
|
Prioritize manual validation for:
|
|
|
|
- tray icon lifecycle and update notifications
|
|
- initialization panel thread cancellation (`prepare.py`, `thread.py`)
|
|
- uninstall flow teardown (M2 crash history)
|
|
- settings window + root password dialog modal behavior
|
|
- QSS theme rendering across all bundled themes
|
|
|
|
## PR Slice Plan (M6)
|
|
|
|
1. **PR-6.1** (this doc + spike)
|
|
2. **PR-6.2** dependency/packaging dual-stack
|
|
3. **PR-6.3** app shell migration
|
|
4. **PR-6.4** threading/async migration
|
|
5. **PR-6.5** main UI migration
|
|
6. **PR-6.6** Wayland/tray validation
|
|
7. **PR-6.7** remove PyQt5 + distribution hardening
|
|
|
|
Each PR must:
|
|
|
|
- compile
|
|
- pass unit tests
|
|
- include a short manual smoke note in PR description
|
|
|
|
## PR-6.1 Spike Procedure
|
|
|
|
Goal: prove Qt6 app boot in <15 minutes on a maintainer machine.
|
|
|
|
Suggested spike steps:
|
|
|
|
```bash
|
|
python3 -m venv /tmp/bearhub-qt6-spike
|
|
/tmp/bearhub-qt6-spike/bin/pip install PySide6 # first install can take several minutes
|
|
|
|
# offscreen run (must quit explicitly; plain exec() blocks)
|
|
QT_QPA_PLATFORM=offscreen /tmp/bearhub-qt6-spike/bin/python -c "
|
|
from PySide6.QtWidgets import QApplication, QLabel
|
|
from PySide6.QtCore import QTimer
|
|
app = QApplication([])
|
|
label = QLabel('Bearhub Qt6 spike')
|
|
label.show()
|
|
QTimer.singleShot(0, app.quit)
|
|
app.exec()
|
|
print('spike-ok')
|
|
"
|
|
```
|
|
|
|
Spike acceptance criteria:
|
|
|
|
- chosen Qt6 binding installs cleanly on Arch
|
|
- minimal window can open offscreen
|
|
- no blocking packaging conflicts with current `python-pyqt5` install path
|
|
- maintainer records binding decision in this document
|
|
|
|
### Spike Result (2026-06-26)
|
|
|
|
Environment:
|
|
|
|
- OS: Arch Linux
|
|
- Python: `3.14.6`
|
|
- Binding installed: `PySide6 6.11.1` (pip wheel in `/tmp/bearhub-qt6-spike`)
|
|
- Arch repo package available: `pyside6 6.11.1-1` (`pacman -Ss pyside6`)
|
|
|
|
Results:
|
|
|
|
| Check | Result |
|
|
|---|---|
|
|
| PySide6 install | Pass (`pip install PySide6`) |
|
|
| Offscreen window bootstrap | Pass (`spike-ok`) |
|
|
| `import bearhub` with PySide6 present in same venv | Pass (metadata import only) |
|
|
| Tray availability in offscreen env | Expected false (`QSystemTrayIcon.isSystemTrayAvailable() == False`) |
|
|
| Theme icon lookup (`bearhub_tray_default`) in offscreen env | Expected null icon |
|
|
|
|
Notes:
|
|
|
|
- Offscreen spike must call `app.quit()` (for example via `QTimer.singleShot`) or the process blocks in `app.exec()`.
|
|
- Tray/icon checks are not valid in offscreen mode; validate in PR-6.6 on a real desktop session (X11/Wayland).
|
|
- Editable install of Bearhub into the spike venv was not required for PR-6.1 and can be deferred to PR-6.2.
|
|
|
|
PR-6.1 status: **accepted** (decision + technical spike complete).
|
|
|
|
## Test Strategy
|
|
|
|
### Automated
|
|
|
|
- keep existing unit tests green in every PR
|
|
- add import smoke test for Qt binding module once dual-stack starts
|
|
- optional CI job later: offscreen `bearhub --version` + window bootstrap
|
|
|
|
### Manual smoke (required per PR from PR-6.3 onward)
|
|
|
|
1. `bearhub` starts to manage panel
|
|
2. search + filter table
|
|
3. install + uninstall small test package
|
|
4. update check from tray and panel
|
|
5. settings open/close
|
|
6. Wayland session test (GNOME/KDE) without forced `xcb` where possible
|
|
|
|
## Packaging Impact
|
|
|
|
| Channel | Current | Target |
|
|
|---|---|---|
|
|
| `pyproject.toml` / `requirements.txt` | `pyqt5` | `PySide6` (or `PyQt6`) |
|
|
| AUR `depends` | `python-pyqt5` | `pyside6` (or `python-pyqt6`) |
|
|
| AppImage | pinned PyQt5 wheel payload | Qt6 runtime bundle aligned with binding |
|
|
| Desktop files | `QT_QPA_PLATFORM=xcb` workaround | remove workaround when Wayland stable |
|
|
|
|
## Rollback Plan
|
|
|
|
- Keep PRs small and independently revertible.
|
|
- During PR-6.2..PR-6.6, allow temporary dual-stack install docs for maintainers.
|
|
- If tray/Wayland regressions appear, revert only the failing PR slice.
|
|
- Do not ship stable release with mixed Qt5/Qt6 runtime dependencies.
|
|
|
|
## Risks and Mitigations
|
|
|
|
| Risk | Impact | Mitigation |
|
|
|---|---|---|
|
|
| Tray behavior differs on Wayland | High | dedicated PR-6.6 matrix tests + fallback docs |
|
|
| `QThread` teardown regressions | High | migrate threading in isolated PR-6.4 with M2 crash retest |
|
|
| QSS theme breakage | Medium | theme snapshot checks for all bundled styles |
|
|
| AppImage size growth with Qt6 | Medium | keep module pruning in AppImage `after_bundle` |
|
|
| License mismatch (PyQt6 GPL) | Legal/process | default to PySide6 unless explicit maintainer decision |
|
|
|
|
## Open Questions
|
|
|
|
- Do we require native Wayland support in first Qt6 stable, or is XWayland acceptable initially?
|
|
- Should CI add GUI smoke with `QT_QPA_PLATFORM=offscreen` in M6 or post-M6?
|
|
- Do we keep temporary `xcb` launcher fallback for one release cycle after Qt6 cutover?
|
|
|
|
## Completion Criteria (M6 Done)
|
|
|
|
- No `PyQt5` imports in runtime modules
|
|
- Bearhub stable release published on Qt6 binding
|
|
- AUR/AppImage/README no longer list PyQt5
|
|
- Wayland smoke test passes without mandatory `QT_QPA_PLATFORM=xcb`
|
|
- PyQt5 marked unsupported in changelog and maintainer docs |