Skip to content

Fix python_qt_binding Qt binding selection on macOS - #86

Closed
baradejoo wants to merge 1 commit into
RoboStack:mainfrom
baradejoo:fix-python-qt-binding-osx
Closed

baradejoo wants to merge 1 commit into
RoboStack:mainfrom
baradejoo:fix-python-qt-binding-osx

Conversation

@baradejoo

Copy link
Copy Markdown
Contributor

Summary

  • patch/ros-lyrical-python-qt-binding.patch: python_qt_binding 2.5.5 (lyrical) hardcodes DEFAULT_BINDING_ORDER = ['pyside'] on macOS, but RoboStack ships PyQt6 and not PySide6, so every Python-based rqt tool fails to start on macOS:
ImportError: Could not find Qt binding (looked for: 'pyside'):
  ImportError for 'pyside': No module named 'PySide6'

The patch is a backport of upstream ros-visualization/python_qt_binding#156 (merged into rolling, not released for lyrical yet), which uses ['pyqt', 'pyside'] on all platforms. ros-jazzy and ros-kilted already carry the same patch (patch/ros-jazzy-python-qt-binding.patch, patch/ros-kilted-python-qt-binding.patch).

  • Bump python_qt_binding build number to 28 so it gets rebuilt.

Remaining runtime issue

With this patch rqt gets past the binding selection, but on osx-arm64 it still crashes at startup during plugin discovery:

$ pixi run rqt
QMetaMethod::invokeMethod: return type mismatch for method Settings::contains(QString): cannot convert from void to bool during invocation
libc++abi: terminating due to uncaught exception of type std::runtime_error: Settings::contains() invoke method failed

The call comes from qt_gui_cpp::RosPluginlibPluginProvider::discover_descriptors() → qt_gui_cpp::Settings::contains(), invoking the Python slot qt_gui.settings.Settings.contains (declared as @Slot(str, result=bool)). Inspecting the metaobject of that instance shows that all its slots are registered with return type void, so the result= of pyqtSlot is lost.

I could reproduce this with plain PyQt6 (conda-forge pyqt6 6.11.0, qt6-main 6.11.2, Python 3.14), without any ROS code, and the result is not deterministic (the same script sometimes reports bool, sometimes void, depending on which objects were created before):

from PyQt6.QtCore import QObject, pyqtSlot, QMutex

class P(QObject):
    def __init__(self):
        super().__init__(); self.setObjectName('SettingsProxy'); self._m = QMutex()
    def contains(self, g, k): return True

class S(QObject):
    def __init__(self):
        super().__init__(); self.setObjectName('Settings')
    @pyqtSlot(str, result=bool)
    def contains(self, key): return True

p = P(); s = S(); mo = s.metaObject()
print(mo.method(mo.indexOfMethod(b'contains(QString)')).typeName())  # 'bool' or 'void'

So it looks like a PyQt6/sip issue rather than something in qt_gui_core. I only tested on osx-arm64, so I don't know whether linux is affected as well.

@traversaro @Tobias-Fischer how would you like to handle this? Should I report it to conda-forge/pyqt-feedstock (or upstream PyQt), or is there a known workaround/pin you'd prefer? This PR only fixes the binding selection, I'm happy to follow up separately.

Notes

Tested locally on osx-arm64: python_qt_binding builds with the patch and rqt now loads PyQt6 instead of failing on the missing PySide6.

@traversaro

Copy link
Copy Markdown
Member

@traversaro @Tobias-Fischer how would you like to handle this? Should I report it to conda-forge/pyqt-feedstock (or upstream PyQt), or is there a known workaround/pin you'd prefer?

Let's do both. If the issue is not reported upstream, please report it, and then if you add a local workaround here, use the issue as a reference in the the workaround you implemented.

@baradejoo

Copy link
Copy Markdown
Contributor Author

I reported it upstream in conda-forge/pyqt-feedstock#201.

It looks specific to the conda-forge builds rather than a Qt or PyQt issue: with pyqt6 6.11.0 _4 + qt6-main 6.11.2 from conda-forge, pyqtSlot(result=...) return types end up registered as void in the meta-object, while the same script works with qt6-main 6.11.1 from conda-forge and with the official PyPI wheels (PyQt6 6.11.0 + PyQt6-Qt6 6.11.2). The issue has a minimal reproducer without ROS.

A local workaround here is possible, but both options I found are not great:

  • Pin qt6_main to 6.11.1 and rebuild the 25 packages that were built against 6.11.2 (similar to Pin qt6-main and pyqt6 to 6.11; rebuild packages built against qt6-main 6.12 #65).
  • Patch qt_gui_cpp::Settings to call the Python slots through QMetaObject::metacall instead of QMetaObject::invokeMethod, which skips the return type check against the broken meta-object. I have a prototype that gets rqt past startup on osx-arm64, but it only fixes this call site, so other places that invoke Python slots with a return value from C++ would still be affected.

So I'd suggest waiting for a fix on the conda-forge side (e.g. a pyqt6 build compatible with qt6-main 6.11.2) and keeping this PR limited to the python_qt_binding fix. If you'd prefer to have a workaround in the meantime, let me know which option you'd like and I'll add it with a reference to the issue.

@baradejoo

Copy link
Copy Markdown
Contributor Author

The PyQt issue is fixed upstream now, so no local workaround is needed here:

The already published lyrical packages don't need a rebuild, they allow pyqt6 >=6.11.0,<6.12.0a0, so a fresh environment picks up _6. This PR only carries the python_qt_binding#156 backport, which is still needed on macOS.

Tested on osx-arm64 in a fresh environment with the published qt_gui_cpp and python_qt_binding from this PR:

$ pixi list | grep -E 'pyqt6|qt6-main|qt-gui-cpp|python-qt-binding'
pyqt6                    6.11.0   py314ha630e2e_6        https://prefix.dev/conda-forge
pyqt6-sip                13.13.0  py314ha630e2e_6        https://prefix.dev/conda-forge
qt6-main                 6.11.2   pl5321hf0f7225_0       https://prefix.dev/conda-forge
ros2-python-qt-binding   2.5.5    np2py314hdc941bd_28    file:///.../ros-lyrical/output
ros2-qt-gui-cpp          2.11.1   np2py314h289bcd9_27    https://prefix.dev/robostack-lyrical
ros2-rqt-gui-cpp         1.10.6   np2py314hda44873_27    https://prefix.dev/robostack-lyrical
$ pixi run rqt

rqt starts and the plugins load. On exit it still aborts with mutex lock failed: Invalid argument (from class_loader::ClassLoader::~ClassLoader() during static destruction). That's a separate macOS issue in class_loader / console_bridge, not related to this PR.

Zrzut ekranu 2026-10-12 o 21 59 44

Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 13, 2026
python_qt_binding only tries PySide on macOS, but RoboStack ships PyQt6 (no
PySide6), so the Python rqt tools failed to start there. Backport of upstream
ros-visualization/python_qt_binding#156, as RoboStack/ros-lyrical#86 does for
lyrical (jazzy already carries it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 13, 2026
…ical#86)

Backport of upstream ros-visualization/python_qt_binding#156: on macOS it only
tried PySide, which RoboStack doesn't ship, so the Python rqt tools failed to start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 13, 2026
…ical#86)

Backport of upstream ros-visualization/python_qt_binding#156: on macOS it only
tried PySide, which RoboStack doesn't ship, so the Python rqt tools failed to start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 13, 2026
…ical#86)

Backport of upstream ros-visualization/python_qt_binding#156: on macOS it only
tried PySide, which RoboStack doesn't ship, so the Python rqt tools failed to start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 13, 2026
…ical#86)

Backport of upstream ros-visualization/python_qt_binding#156: on macOS it only
tried PySide, which RoboStack doesn't ship, so the Python rqt tools failed to start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 13, 2026
…ical#86)

Backport of upstream ros-visualization/python_qt_binding#156: on macOS it only
tried PySide, which RoboStack doesn't ship, so the Python rqt tools failed to start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 16, 2026
`rs lyrical update-snapshot` on top of #82, with the fixes from the full-rebuild runs:
- lyrical: rtabmap without its tests (MSVC can't build test_optimizer_gtsam.cpp), as jazzy
- lyrical: python_qt_binding uses PyQt6 on macOS too (RoboStack/ros-lyrical#86)
- rcutils test: Python import only on humble and jazzy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 16, 2026
`rs lyrical update-snapshot`, with the fixes from the full-rebuild runs:
- lyrical: rtabmap without its tests (MSVC can't build test_optimizer_gtsam.cpp), as jazzy
- lyrical: python_qt_binding uses PyQt6 on macOS too (RoboStack/ros-lyrical#86)
- rcutils test: Python import only on humble and jazzy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Tobias-Fischer

Copy link
Copy Markdown
Contributor

See RoboStack/robostack#60

@baradejoo
baradejoo deleted the fix-python-qt-binding-osx branch October 18, 2026 10:50
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 23, 2026
* lyrical: on the shared conda-forge pinning, Qt6 mapping shared (from #82)

Drops the distribution's own conda-forge-pinning version and migrations: it uses
shared/pinning/conda_forge.yaml (2026.10.09.13.11.31), with the package fixes the new
pins needed. Other distributions keep their own pinning version until their PR; their
conda_build_config.yaml only gets the comment of the new shared openexr override
(the value, 3.4, is what they pin already).

shared/robostack.yaml maps the Qt5 rosdep keys to Qt6 (qt6-main, pyqt6), so lyrical's
per-package Qt workarounds go. humble and jazzy keep Qt5 through overrides that their
own sync PRs add: merge those first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* lyrical: rosdistro sync 2026-10-05

`rs lyrical update-snapshot`, with the fixes from the full-rebuild runs:
- lyrical: rtabmap without its tests (MSVC can't build test_optimizer_gtsam.cpp), as jazzy
- lyrical: python_qt_binding uses PyQt6 on macOS too (RoboStack/ros-lyrical#86)
- rcutils test: Python import only on humble and jazzy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* lyrical: foxglove_bridge on Windows with /permissive

With C++20 (pulled in by lyrical's dependencies) MSVC enables /permissive-, which
rejects the Foxglove SDK 0.28's explicit specializations of ParameterView::get after
their first use (C2908/C3856 in parameter.hpp); humble/jazzy build it as C++17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* lyrical: Rust message crates match the rosidl_buffer sequence layout [skip ci]

From Lyrical on, rosidl_runtime_c's primitive and string sequences carry two
trailing rosidl_buffer flags (ros2/rosidl#942), 32 instead of 24 bytes.
rosidl_runtime_rs 0.7 has the matching layout (ros2-rust/rosidl_runtime_rs#22),
and rclrs 0.8 uses it, but rosidl_generator_rs 0.5.0 still pins the generated
crates to 0.6, so every primitive-sequence field is misread: a Marker or
JointState round trip segfaults (RoboStack/ros-lyrical#17,
ros2-rust/ros2_rust#659). Pin 0.7. The generated code needs no other change.

Checked locally: the generated std_msgs crate pins 0.7, and with 0.7 the
visualization_msgs/Marker and sensor_msgs/JointState round trips are correct
(rmw Marker 480 bytes, as in C), while 0.6 segfaults.

Also point gz_ros2_control's plugin path hook at bin/ on Windows, where the
plugin DLL is installed (#104).

Evict the generator, every package that generates interfaces (each ships its
generated Rust crate) and gz_ros2_control from the PR build cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Update mutex_package configuration in vinca.yaml

Removed build number and upper bound for mutex_package.

* Clear evict_cache in ci.yaml

Reset evict_cache to an empty list.

* humble and rolling keep Qt5 for the Qt5 rosdep keys

This PR maps the Qt5 rosdep keys to Qt6 for every distribution. humble's own
override comes with its sync (#62), rolling moves to Qt6 with #99: until then
both get the override jazzy has (distros/<d>/robostack.yaml, layered over the
shared mapping with conda_index). Their generated recipes (humble linux-64,
win-64, osx-arm64; rolling linux-64, win-64) are identical to main's, except
for the new rcutils package test, which doesn't change any build hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* rosbag2_performance_benchmarking_msgs: no own short-paths flag

vinca's Windows build scripts pass -DCMAKE_INTERMEDIATE_DIR_STRATEGY=SHORT for
every package since RoboStack/vinca#178 (pinned with #139, which has to be
merged before this PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Build branches: a newer run cancels the older one

A new push to a build branch (after a merge to main) brings the latest
recipes and workflow; the older run's plan is out of date. The new run skips
what the old one uploaded already, so only the packages its cancelled jobs
had built but not uploaded are built again. Re-run failed jobs only in the
newest run: re-running an older one would cancel the newer run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Build times from the jazzy full rebuild (linux-64, linux-aarch64, osx-arm64)

Per-recipe build times of the jazzy build-branch runs 39770179894,
39770118028 and 39770103102, from their job logs, so that this sync's
lyrical rebuild is already packed by time (osx-64 and win-64 weren't
finished yet). The weekly update-build-timings PR adds the times the build
jobs record from now on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Build times for all five platforms, also for lyrical's compatibility recipes

osx-64 and win-64 from the jazzy build-branch runs' job logs (39770120710 and
39907983469; 39770136120 and the finished jobs of 39907979075). Recipes those
runs didn't reach are estimated from their linux-64 time with a fit per
platform (osx-64: 37 s + 1.61 x; win-64: 77 s + 2.13 x; median error 33% and
20%), compatibility recipes at the platform's median (0.3-0.4 s).

Every ros-jazzy-<name> time is copied to ros-lyrical-<name> (under
"lyrical"), so that lyrical's compatibility recipes have a time until its own
build jobs record theirs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test_generate_gha: independent of the repository's build times

With shared/build_timings/win-64.json present, rs gha packs by time and first
retries an oversized workflow with fewer runners, so the batch-size retry test
now runs without timings; a new test covers the retry with timings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* rs gha: fall back to batching by count when a timed workflow stays too large

Packing by time keeps short stages apart (they share a job only up to two
minutes), so lyrical's win-64 workflow, whose jobs carry the long Windows
scripts, stayed at 621 KiB even with --runners 2. After the runner retries,
generate it without --timings; batching by count then merges the short
stages and the larger-batch retries shrink it as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* vinca with job budgets (RoboStack/vinca#180); rs gha without its own size retries

vinca-gha now keeps the workflow under GitHub's size limit itself (a job budget
spent where it shortens the workflow most, with or without build times), and
its Windows jobs call .scripts/build_win.bat instead of inlining it. rs gha
drops the retries with fewer runners and the fallback to batching by count;
larger batches remain the last resort when even the fewest jobs the batch
size allows don't fit. The check that the Windows workflow uses the
repository's build script looks for the call.

lyrical's full rebuild: win-64 149 jobs (483 KiB), linux-64 197 jobs (341 KiB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Tobias Fischer <info@tobiasfischer.info>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants