migrate pyqt5 to pyside6

This commit is contained in:
ykcheungab
2026-09-21 09:34:56 +08:00
parent e276b5de0f
commit ccedb9ddd9
66 changed files with 2184 additions and 395 deletions
+13 -4
View File
@@ -24,7 +24,7 @@ dist/
OrisysStudio/
OrisysStudio.exe
config/ # lean locales + sensors (no finger visualization/)
_internal/ # Python, PyQt5, opencv-headless, orisys, OpenGL, …
_internal/ # Python, PySide6, opencv-headless, orisys, OpenGL, …
```
## Why there are two steps
@@ -137,11 +137,20 @@ loaded" can only be printed if those modules imported.)
## Dependency notes
- App shared dep uses **`opencv-python-headless`** (PyQt owns the GUI). `setup.ps1` uninstalls
any `opencv-python` and pins `opencv-python-headless>=4.10,<5` (5.x has shipped wheels
without a `cv2` module) and verifies `import cv2`.
- **PySide6** is the Qt binding (LGPLv3). PyQt5 was replaced because its free option is
GPLv3-or-commercial; the apps are PySide6-only and their tests enforce it
(`apps/studio/tests/test_binding_purity.py`, `apps/autocal/tests/test_binding_purity.py`).
`setup.ps1` **uninstalls** PyQt5/PyQt6/PySide2 from the build venv and verifies only one
binding is present: `pyqtgraph` binds to whichever toolkit it imports first, so a leftover
PyQt5 yields a bundle whose plots were built from the wrong binding. The spec also excludes
`PyQt5`/`PyQt6`/`PySide2` outright, and must never exclude `PySide6` itself.
- App shared dep uses **`opencv-python-headless`** (PySide6 owns the GUI). `setup.ps1`
uninstalls any `opencv-python` and pins `opencv-python-headless>=4.10,<5` (5.x has shipped
wheels without a `cv2` module) and verifies `import cv2`.
- **PyOpenGL** is the GL binding for the 3D pane and comes from the studio's `three-d` extra.
Add the `protected-assets` extra (`cryptography`) if sealed `.oryscene` packages are shipped.
- **`QOpenGLWidget` lives in `PySide6.QtOpenGLWidgets`**, not `QtWidgets` (Qt6 moved it): the
spec names it in `hiddenimports` so the module and `Qt6OpenGLWidgets.dll` are bundled.
- **Open3D stays excluded** (spec + build env): it is an offline geometry tool. The 3D pane is
Qt `QOpenGLWidget` + PyOpenGL.
- `constraints-build.txt` pins the toolchain. Move a pin only together with `setup.bat -Force`
+9 -4
View File
@@ -4,7 +4,11 @@
# on the spec (which Qt, which numpy, which hooks-contrib), so a build that "works" on one
# machine and produces a non-starting EXE on another is usually a version difference that
# nobody wrote down. `-c` constrains whatever else asks for these packages, so the app's
# own loose requirements (PyQt5>=5.15, scipy>=1.10.1, ...) resolve to exactly this set.
# own loose requirements (PySide6>=6.6, scipy>=1.10.1, ...) resolve to exactly this set.
#
# PySide6 rather than PyQt5: the apps are LGPLv3-bound. PyQt5 must not be present in the
# build environment at all — `pyqtgraph` binds to whichever toolkit it finds first, so a
# leftover PyQt5 produces plots built from the wrong binding (setup.ps1 uninstalls it).
#
# `orisys` is deliberately absent: it is installed from a local checkout (or deliberately
# from PyPI with -FromPypi), and pinning it by name would fight the editable install.
@@ -15,9 +19,10 @@
PyInstaller==6.22.2
pyinstaller-hooks-contrib==2026.7
PyOpenGL==3.1.10
PyQt5==5.15.11
PyQt5-Qt5==5.15.2
PyQt5-sip==12.19.0
PySide6==6.11.2
PySide6-Essentials==6.11.2
PySide6-Addons==6.11.2
shiboken6==6.11.2
numpy==2.2.6
scipy==1.15.3
numba==0.67.0
+49 -16
View File
@@ -79,7 +79,12 @@ datas = _branding_datas()
binaries: list = _extra_runtime_binaries()
hiddenimports = [
"cv2",
"PyQt5.sip",
# PySide6's runtime is shiboken6; the Qt binding itself is found from the imports, but
# QtOpenGLWidgets is unusual enough to name explicitly: it is a separate module and DLL
# (Qt6OpenGLWidgets), and without it the 3D pane reports "no OpenGL widgets" while the
# rest of the app looks fine.
"shiboken6",
"PySide6.QtOpenGLWidgets",
"pyqtgraph.graphicsItems",
"scipy.signal",
"numba",
@@ -126,22 +131,50 @@ a = Analysis(
"jupyter",
"notebook",
"pytest",
"PyQt5.QtBluetooth",
"PyQt5.QtNfc",
"PyQt5.QtPositioning",
"PyQt5.QtLocation",
"PyQt5.QtMultimedia",
"PyQt5.QtMultimediaWidgets",
"PyQt5.QtSensors",
"PyQt5.QtSerialPort",
"PyQt5.QtWebChannel",
"PyQt5.QtWebEngine",
"PyQt5.QtWebEngineCore",
"PyQt5.QtWebEngineWidgets",
"PyQt5.QtWebSockets",
"PySide2",
"PySide6",
# The app uses PySide6 only. Qt modules it never imports are excluded to keep the
# bundle small — but `"PySide6"` itself must NOT appear here: excluding the whole
# binding produces an EXE with no Qt at all (it fails at start-up, not at build
# time). `PySide6.QtOpenGLWidgets` is required by the 3D pane and is deliberately
# absent from this list.
"PySide6.Qt3DAnimation",
"PySide6.Qt3DCore",
"PySide6.Qt3DExtras",
"PySide6.Qt3DInput",
"PySide6.Qt3DLogic",
"PySide6.Qt3DRender",
"PySide6.QtBluetooth",
"PySide6.QtCharts",
"PySide6.QtDataVisualization",
"PySide6.QtGraphs",
"PySide6.QtHelp",
"PySide6.QtLocation",
"PySide6.QtMultimedia",
"PySide6.QtMultimediaWidgets",
"PySide6.QtNfc",
"PySide6.QtPdf",
"PySide6.QtPdfWidgets",
"PySide6.QtPositioning",
"PySide6.QtQml",
"PySide6.QtQuick",
"PySide6.QtQuick3D",
"PySide6.QtQuickControls2",
"PySide6.QtQuickWidgets",
"PySide6.QtRemoteObjects",
"PySide6.QtScxml",
"PySide6.QtSensors",
"PySide6.QtSerialBus",
"PySide6.QtSerialPort",
"PySide6.QtSpatialAudio",
"PySide6.QtStateMachine",
"PySide6.QtTextToSpeech",
"PySide6.QtWebChannel",
"PySide6.QtWebEngineCore",
"PySide6.QtWebEngineQuick",
"PySide6.QtWebEngineWidgets",
"PySide6.QtWebSockets",
"PyQt5",
"PyQt6",
"PySide2",
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
+13 -2
View File
@@ -237,12 +237,19 @@ $install += @($PyInstallerPin, $CvPin)
& $VenvPy @install
if ($LASTEXITCODE -ne 0) { throw "pip install failed" }
# Upstream `orisys` may declare opencv-python; the app needs the headless wheel (PyQt owns
# Upstream `orisys` may declare opencv-python; the app needs the headless wheel (PySide6 owns
# the GUI), and 5.x can arrive without a `cv2` module.
& $VenvPy -m pip uninstall -y opencv-python opencv-python-headless 2>$null | Out-Null
& $VenvPy -m pip install --disable-pip-version-check -c $Constraints $CvPin
if ($LASTEXITCODE -ne 0) { throw "opencv-python-headless install failed" }
# PyQt5 must not be in the build environment, even though the SDK's `gui` extra asks for it
# when someone installs that: `pyqtgraph` binds to whichever toolkit it imports first, and a
# frozen app built where PyQt5 is present can end up with plots built from the wrong binding.
# The apps are PySide6-only (see apps/studio/tests/test_binding_purity.py).
Write-Host "Removing any other Qt binding from the build venv ..."
& $VenvPy -m pip uninstall -y PyQt5 PyQt5-Qt5 PyQt5-sip PyQt6 PySide2 2>$null | Out-Null
# Open3D must not be in the build env: it is an offline tool, and the spec excludes it.
$hasOpen3d = & $VenvPy -c "import importlib.util; print(importlib.util.find_spec('open3d') is not None)"
if ($hasOpen3d -eq "True") {
@@ -256,8 +263,12 @@ if (-not $NoVerify) {
Write-Host "Verifying the provisioned environment ..."
# These are the imports the frozen app makes before it can draw anything. Checking them
# here is what turns "the build succeeded" into "the build can start".
& $VenvPy -c "import orisys_app_shared, orisys_studio, orisys, OpenGL, cv2, PyQt5.QtWidgets, PyInstaller; print('imports OK')"
& $VenvPy -c "import orisys_app_shared, orisys_studio, orisys, OpenGL, cv2, PySide6.QtWidgets, PySide6.QtOpenGLWidgets, PyInstaller; print('imports OK')"
if ($LASTEXITCODE -ne 0) { throw "the venv cannot import what the app imports (see above)" }
# A second binding in the venv is the failure mode the uninstall above exists for: it
# imports fine here and breaks `pyqtgraph` at run time.
& $VenvPy -c "import importlib.util as u; bad=[m for m in ('PyQt5','PyQt6','PySide2') if u.find_spec(m)]; assert not bad, 'another Qt binding is installed: '+str(bad); print('binding OK: PySide6 only')"
if ($LASTEXITCODE -ne 0) { throw "the build venv has more than one Qt binding" }
# Theme colours are package data: PyInstaller only bundles them if the package is
# importable at build time, so a stale editable install loses them silently.
& $VenvPy -c "from PyInstaller.utils.hooks import collect_data_files as c; d=c('orisys_app_shared', includes=['theme/*.json']); assert d, 'theme data not collectable'; print('theme data OK:', len(d), 'files')"