Skip to content

CMakeLists: dual-support Qt5 and Qt6 - #300

Merged
nbbrooks merged 2 commits into
ros2from
nbbrooks/rolling-qt6-dual-support
Aug 2, 2026
Merged

nbbrooks merged 2 commits into
ros2from
nbbrooks/rolling-qt6-dual-support

Conversation

@nbbrooks

@nbbrooks nbbrooks commented Aug 2, 2026

Copy link
Copy Markdown
Member

Why

On Rolling/Resolute, rviz is built against Qt6. rviz_visual_tools's CMakeLists.txt calls find_package(Qt5 REQUIRED COMPONENTS Widgets) unconditionally, so the two collide and CMake refuses to generate:

CMake Error: The INTERFACE_QT_MAJOR_VERSION property of "Qt5::Core" does
not agree with the value of QT_MAJOR_VERSION already determined
CMake Generate step failed.  Build files cannot be regenerated correctly.

That's from a real rolling-testing run on ubuntu:resolute (#301, job rolling-testing + ccov). Everything transitively depending on this package — moveit_visual_tools, moveit_task_constructor_visualization, snowbot_operating_system — is blocked behind it.

Correction to an earlier version of this description, which claimed Resolute had removed Qt5 from the apt indexes. That is not true — the same CI run shows apt-get install qtbase5-dev succeeding on ubuntu:resolute (qt5-qmake-bin 5.15.18+dfsg-1ubuntu1). Qt5 is still installable; the problem is purely that it cannot be mixed with the Qt6 rviz we link against. The fix is unchanged, but the reasoning matters for reviewers.

This is the last Rolling-compat blocker for this repo. The other pieces an earlier audit flagged — ament_target_dependencies migration (#277), tf2 .h.hpp renames (#284), CMake required-version bump (#287) — have all already landed on ros2.

How

Select Qt6 when building against rviz >= 15.1.14, otherwise keep Qt5.

That's the release where rviz itself switched: ros2/rviz#1635 "Use qt6 as the default dependency from rosdep" (ahcorde, merged 2025-12-08, 4784c8cd) flipped rviz_common/package.xml from qtbase5-dev/libqt5-* to qt6-base-dev/libqt6-*, and first shipped in the 15.1.14 tag on 2025-12-17.

Note the threshold is 15.1.14, not 15.1 — bisecting upstream rviz_common/package.xml shows 15.1.0 through 15.1.13 are still Qt5:

15.1.11 15.1.12 15.1.13 15.1.14
qtbase5-dev qtbase5-dev qtbase5-dev qt6-base-dev

Why gate on rviz's version rather than on Qt6 availability

An availability probe (find_package(Qt6 QUIET)if(Qt6_FOUND)) says nothing about which Qt rviz was built against. rviz_visual_tools_gui links rviz_common, rviz_rendering, rviz_default_plugins and rviz_ogre_vendor::OgreMain, so its Qt has to match rviz's.

Ubuntu 24.04 ships both Qt5 and Qt6. On a jazzy or kilted machine that also has qt6-base-dev installed, the availability probe doesn't quietly mis-select — it aborts the configure, because rviz_common (Qt5) has already defined the versionless Qt::Core target:

CMake Error at Qt6CoreVersionlessTargets.cmake:42 (message):
  Some (but not all) targets in this export set were already defined.
  Targets Defined: Qt::Core
  Targets not yet defined: Qt::CorePrivate

QUIET does not suppress that — it's a FATAL_ERROR inside Qt6's own config, not a not-found condition, so the Qt5 fallback is never reached.

This mirrors the approach in moveit/moveit2#3707, including the QT_DIR hint for transitive deps whose find_package(QT NAMES Qt6 Qt5 ...) can mis-resolve to Qt5 under CMake's ascending path order.

Validation

Probed osrf/ros:<distro>-desktop containers, comparing the gate's choice against objdump -p librviz_common.so:

distro rviz_common_VERSION gate selects rviz actually links
humble 11.2.26 Qt5 libQt5Core
jazzy 14.1.22 Qt5 libQt5Core
kilted 15.0.13 Qt5 libQt5Core
lyrical 15.2.4 Qt6 libQt6Core
rolling 16.0.1 Qt6 libQt6Core

Also verified that with qt6-base-dev additionally installed on jazzy and kilted, this gate still selects Qt5 and configures cleanly.

package.xml: versionless Qt rosdep keys

The CMake gate alone is not sufficient. package.xml hard-coded Qt5 rosdep keys:

<build_depend>qtbase5-dev</build_depend>
<exec_depend>libqt5-widgets</exec_depend>

Those resolve to Qt5 on every platform, so on Resolute rosdep installs Qt5 while CMake needs Qt6 — which is precisely how the INTERFACE_QT_MAJOR_VERSION collision above arises. (Confirmed in CI: apt-get install qtbase5-dev and libqt5widgets5t64 both succeed on Resolute, then the build fails at generate.)

Switched to the versionless keys, the same move ros2/rviz#1635 made in rviz_common's own package.xml. rosdep maps them per-platform:

key jammy noble * (resolute)
qt-base-dev qtbase5-dev qtbase5-dev qt6-base-dev
libqtwidgets libqt5widgets5 libqt5widgets5t64 libqt6widgets6

Which agrees with the CMake gate on all five distros — rosdep installs exactly the Qt that CMake then selects:

distro base rviz CMake picks rosdep installs
humble jammy 11.2 Qt5 Qt5
jazzy noble 14.1 Qt5 Qt5
kilted noble 15.0 Qt5 Qt5
lyrical resolute 15.2 Qt6 Qt6
rolling resolute 16.0 Qt6 Qt6

Scope

Build metadata only — CMakeLists.txt + package.xml. No C++ source changes are needed: this package has no .ui files (so no qt5_wrap_ui) and none of the Qt5-only API patterns moveit2 had to fix — only plain widget includes, Q_OBJECT, QCursor and Qt::Key_N enums, all unchanged in Qt6.

Behavior on humble/jazzy/kilted is unchanged.

CI caveat

This repo's ros2 matrix currently reports nothing: it lacks fail-fast: false, and its rolling + ROS_REPO: main job cannot run on Resolute, so it fails in ~60s and cancels every sibling. #301 fixes that. This PR should be rebased on #301 before merging, so its Qt change actually gets validated rather than cancelled.

Related

Overlaps #282 (competing Qt6 PR, 8 files, drops the Qt5 fallback and bumps C++ standard). This PR is intentionally minimal and preserves Qt5 for the distros that still need it; #288 reports #282 still doesn't build Qt6 cleanly. Suggest closing #282 in favor of this, and rebasing the CI-actions bump it carries onto ros2 separately if still wanted.

🤖 Generated with Claude Code

@nbbrooks
nbbrooks force-pushed the nbbrooks/rolling-qt6-dual-support branch from a517835 to c7f702f Compare August 2, 2026 20:03
@nbbrooks
nbbrooks force-pushed the nbbrooks/rolling-qt6-dual-support branch from 85207b8 to 92ddda6 Compare August 2, 2026 22:02
nbbrooks added a commit that referenced this pull request Aug 2, 2026
Three problems, all of which make CI on `ros2` uninformative today.

1. fail-fast

The matrix had no `fail-fast: false`, so it defaulted to true. The rolling
job fails in ~60s (problem 2) and GitHub then cancels every sibling before
it can report. From PR #300, run 30764760558 attempt 3:

  rolling-main + ccov  20:49:41 -> 20:50:45  failure
  humble-main          20:49:47 -> 20:50:51  cancelled
  humble-testing       20:49:47 -> 20:50:52  cancelled
  rolling-testing      20:49:41 -> 20:50:52  cancelled

The matrix reported nothing at all, and re-running reproduced it exactly.

2. rolling + ROS_REPO: main on Resolute

Rolling's base OS moved to Ubuntu Resolute. Rolling's `main` apt repo has no
Resolute packages yet, so the job dies before CMake:

  'sudo apt-get install ... ros-rolling-ros-environment' returned with 100
  'setup_rosdep' returned with code '100' after 0 min 7 sec

Not PR-specific: PR #297 fails the identical job. Switch it to `testing`,
which does work on Resolute, and pin OS_CODE_NAME explicitly rather than
relying on industrial_ci's default -- that default has changed before.

Mark it non-blocking, matching the policy moveit2 applies to its own
rolling-resolute job ("non-blocking until all Resolute packages are
released"). CCOV rides on this job, so coverage is non-blocking too.

3. Three released distros had no CI at all

This repo ships a single `ros2` branch to five distros but only tested two.
jazzy (4.1.4-4), kilted (4.1.4-4) and lyrical (4.1.4-5) are all released
from this branch with zero coverage. Unlike moveit2 -- which can omit
distros from `main` because it maintains per-distro branches -- there is no
second branch here to pick up the slack.

That gap lands exactly on the case that matters most for the Qt work:
jazzy and kilted are Ubuntu Noble, the platform where Qt5 and Qt6 can be
installed side by side, which is precisely what the rviz-version Qt gate in
CMakeLists guards against. humble covers jammy/Qt5 and rolling covers
resolute/Qt6; the interesting middle was untested.

lyrical is Resolute and released, so its `main` repo is populated -- it
gives a blocking Qt6-on-Resolute job that should stay green, instead of
leaving all Qt6 coverage on the non-blocking rolling job.

Restore the rolling+main entry once Rolling publishes to `main` for
Resolute, and drop the NONBLOCKING flag at the same time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nbbrooks added a commit that referenced this pull request Aug 2, 2026
Three problems, all of which make CI on `ros2` uninformative today.

1. fail-fast

The matrix had no `fail-fast: false`, so it defaulted to true. The rolling
job fails in ~60s (problem 2) and GitHub then cancels every sibling before
it can report. From PR #300, run 30764760558 attempt 3:

  rolling-main + ccov  20:49:41 -> 20:50:45  failure
  humble-main          20:49:47 -> 20:50:51  cancelled
  humble-testing       20:49:47 -> 20:50:52  cancelled
  rolling-testing      20:49:41 -> 20:50:52  cancelled

The matrix reported nothing at all, and re-running reproduced it exactly.

2. rolling + ROS_REPO: main on Resolute

Rolling's base OS moved to Ubuntu Resolute. Rolling's `main` apt repo has no
Resolute packages yet, so the job dies before CMake:

  'sudo apt-get install ... ros-rolling-ros-environment' returned with 100
  'setup_rosdep' returned with code '100' after 0 min 7 sec

Not PR-specific: PR #297 fails the identical job. Switch it to `testing`,
which does work on Resolute, and pin OS_CODE_NAME explicitly rather than
relying on industrial_ci's default -- that default has changed before.

Mark it non-blocking, matching the policy moveit2 applies to its own
rolling-resolute job ("non-blocking until all Resolute packages are
released"). CCOV rides on this job, so coverage is non-blocking too.

3. Three released distros had no CI at all

This repo ships a single `ros2` branch to five distros but only tested two.
jazzy (4.1.4-4), kilted (4.1.4-4) and lyrical (4.1.4-5) are all released
from this branch with zero coverage. Unlike moveit2 -- which can omit
distros from `main` because it maintains per-distro branches -- there is no
second branch here to pick up the slack.

That gap lands exactly on the case that matters most for the Qt work:
jazzy and kilted are Ubuntu Noble, the platform where Qt5 and Qt6 can be
installed side by side, which is precisely what the rviz-version Qt gate in
CMakeLists guards against. humble covers jammy/Qt5 and rolling covers
resolute/Qt6; the interesting middle was untested.

lyrical is Resolute and released, so its `main` repo is populated -- it
gives a blocking Qt6-on-Resolute job that should stay green, instead of
leaving all Qt6 coverage on the non-blocking rolling job.

Restore the rolling+main entry once Rolling publishes to `main` for
Resolute, and drop the NONBLOCKING flag at the same time.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
nbbrooks and others added 2 commits August 2, 2026 16:22
Select Qt6 when building against rviz >= 15.1.14, which is the release where
rviz itself switched to Qt6; otherwise keep Qt5.

On Rolling / Ubuntu Resolute, rviz is built against Qt6, so this package's
unconditional find_package(Qt5 REQUIRED COMPONENTS Widgets) collides with it
and CMake refuses to generate at all:

  CMake Error: The INTERFACE_QT_MAJOR_VERSION property of "Qt5::Core" does
  not agree with the value of QT_MAJOR_VERSION already determined
  CMake Generate step failed.  Build files cannot be regenerated correctly.

Note the problem is the Qt5/Qt6 *mix*, not Qt5 being unavailable: Qt5 is
still perfectly installable on Resolute (CI shows apt-get install qtbase5-dev
pulling qt5-qmake-bin 5.15.18+dfsg-1ubuntu1). Availability is therefore the
wrong thing to gate on -- see below.

The Qt6 switch is ros2/rviz#1635 "Use qt6 as the default dependency from
rosdep" (ahcorde, merged 2025-12-08, commit 4784c8cd), which flipped
rviz_common's package.xml from qtbase5-dev/libqt5-* to qt6-base-dev/libqt6-*.
It first shipped in rviz 15.1.14 (tagged 2025-12-17) -- note 15.1.0 through
15.1.13 are still Qt5, so the threshold is 15.1.14 and not 15.1.

Gate on rviz_common_VERSION rather than on whether Qt6 is installed. An
availability probe (find_package(Qt6 QUIET)) says nothing about which Qt rviz
was built against, and rviz_visual_tools_gui links rviz_common, rviz_rendering,
rviz_default_plugins and rviz_ogre_vendor::OgreMain, so its Qt must match
rviz's. On a jazzy or kilted machine that also has qt6-base-dev installed, the
availability probe does not silently mis-select -- it aborts the configure,
because rviz_common has already defined the versionless Qt::Core target:

  CMake Error at Qt6CoreVersionlessTargets.cmake:42:
    Some (but not all) targets in this export set were already defined.
    Targets Defined: Qt::Core
    Targets not yet defined: Qt::CorePrivate

QUIET does not suppress that -- it is a FATAL_ERROR inside Qt6's own config,
not a not-found condition, so the Qt5 fallback is never reached.

Validated in osrf/ros:<distro>-desktop containers, comparing the gate's choice
against objdump of librviz_common.so:

  distro   rviz_common  gate  rviz links
  humble   11.2.26      Qt5   libQt5Core
  jazzy    14.1.22      Qt5   libQt5Core
  kilted   15.0.13      Qt5   libQt5Core
  lyrical  15.2.4       Qt6   libQt6Core
  rolling  16.0.1       Qt6   libQt6Core

Also verified that with qt6-base-dev additionally installed on jazzy and
kilted, this gate still selects Qt5 and configures cleanly.

Mirrors the approach taken in moveit/moveit2#3707, including the QT_DIR hint
for transitive deps whose find_package(QT NAMES Qt6 Qt5 ...) can mis-resolve
to Qt5 under CMake's ascending path order.

No source-side Qt6 changes are needed here: this package has no .ui files and
none of the Qt5-only API patterns moveit2 had to fix -- only plain widget
includes, Q_OBJECT, QCursor and Qt::Key_N enums, all unchanged in Qt6.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CMake change alone is not enough. package.xml still hard-coded Qt5
rosdep keys:

  <build_depend>qtbase5-dev</build_depend>
  <exec_depend>libqt5-widgets</exec_depend>

Those resolve to Qt5 on every platform, so on Resolute rosdep installs Qt5
while CMake needs Qt6 to match rviz -- which is exactly how the
INTERFACE_QT_MAJOR_VERSION collision fixed in the previous commit arises.
CI confirms the sequence: apt-get install qtbase5-dev and libqt5widgets5t64
both succeed on ubuntu:resolute, and the build then fails at generate.

(To be clear, Qt5 has not been removed from Resolute's apt indexes; it
installs fine. It simply must not be mixed with the Qt6 rviz we link.)

Switch to the versionless keys, which rosdep maps per-platform. This is the
same move ros2/rviz#1635 made in rviz_common's package.xml:

  key             jammy           noble               * (resolute)
  qt-base-dev     qtbase5-dev     qtbase5-dev         qt6-base-dev
  libqtwidgets    libqt5widgets5  libqt5widgets5t64   libqt6widgets6

That mapping agrees with the CMake gate on all five distros, so rosdep
installs exactly the Qt that CMake then selects:

  humble  (jammy)    rviz 11.2  -> Qt5
  jazzy   (noble)    rviz 14.1  -> Qt5
  kilted  (noble)    rviz 15.0  -> Qt5
  lyrical (resolute) rviz 15.2  -> Qt6
  rolling (resolute) rviz 16.0  -> Qt6

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nbbrooks
nbbrooks force-pushed the nbbrooks/rolling-qt6-dual-support branch from 92ddda6 to 95b28d1 Compare August 2, 2026 22:22
@nbbrooks

nbbrooks commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

CI error predates this change.

@nbbrooks
nbbrooks merged commit dbfb009 into ros2 Aug 2, 2026
6 of 7 checks passed
nbbrooks added a commit that referenced this pull request Aug 2, 2026
tf2_ros::TransformBroadcaster used to accept a node pointer through a set of
constructor overloads marked

  [[deprecated("Use rclcpp::node_interfaces::NodeInterfaces instead of NodeT")]]

guarded by std::enable_if_t<rcpputils::is_pointer<NodeT>::value, bool>.
ros2/geometry2#940 "Removed deprecated code" (ahcorde, merged 2026-07-01,
+0/-978 across 13 files) deleted them. What remains takes
rclcpp::node_interfaces::NodeInterfaces<NodeParametersInterface,
NodeTopicsInterface>, which calls get_node_*_interface() directly on whatever
it is handed -- so a std::shared_ptr<rclcpp::Node> no longer satisfies it:

  /opt/ros/rolling/include/rclcpp/rclcpp/node_interfaces/node_topics_interface.hpp:99:1:
  error: 'const class std::shared_ptr<rclcpp::Node>' has no member named
         'get_node_topics_interface'
     99 | RCLCPP_NODE_INTERFACE_HELPERS_SUPPORT(
          rclcpp::node_interfaces::NodeTopicsInterface, topics)

Dereference instead. The remaining constructor is templated on node type and
accepts a node reference on every supported distro, so no version guard is
needed -- and this is the migration the deprecation notice asked for.

First shipped in tf2_ros 0.46.2:

  0.46.1  pointer ctor present  -> old form compiles
  0.46.2  removed by #940       -> old form fails
  0.46.3  removed               -> old form fails

Verified by compiling both forms against each distro's real headers:

  distro                              (node)      (*node)
  humble                              ok          ok
  jazzy                               ok          ok
  rolling  osrf/ros:rolling-desktop   ok          ok    (tf2_ros 0.46.1)
  rolling  ros-testing on resolute    FAILS       ok    (tf2_ros 0.46.3)

Note the prebuilt rolling desktop image still ships 0.46.1 and therefore
still accepts the old form; only the current ros-testing packages on
Resolute reject it, which is what CI builds against.

Surfaced by the rolling-testing job on #300 once #301 stopped fail-fast
from cancelling it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nbbrooks added a commit that referenced this pull request Aug 2, 2026
tf2_ros::TransformBroadcaster used to accept a node pointer through a set of
constructor overloads marked

  [[deprecated("Use rclcpp::node_interfaces::NodeInterfaces instead of NodeT")]]

guarded by std::enable_if_t<rcpputils::is_pointer<NodeT>::value, bool>.
ros2/geometry2#940 "Removed deprecated code" (ahcorde, merged 2026-07-01,
+0/-978 across 13 files) deleted them. What remains takes
rclcpp::node_interfaces::NodeInterfaces<NodeParametersInterface,
NodeTopicsInterface>, which calls get_node_*_interface() directly on whatever
it is handed -- so a std::shared_ptr<rclcpp::Node> no longer satisfies it:

  /opt/ros/rolling/include/rclcpp/rclcpp/node_interfaces/node_topics_interface.hpp:99:1:
  error: 'const class std::shared_ptr<rclcpp::Node>' has no member named
         'get_node_topics_interface'
     99 | RCLCPP_NODE_INTERFACE_HELPERS_SUPPORT(
          rclcpp::node_interfaces::NodeTopicsInterface, topics)

Dereference instead. The remaining constructor is templated on node type and
accepts a node reference on every supported distro, so no version guard is
needed -- and this is the migration the deprecation notice asked for.

First shipped in tf2_ros 0.46.2:

  0.46.1  pointer ctor present  -> old form compiles
  0.46.2  removed by #940       -> old form fails
  0.46.3  removed               -> old form fails

Verified by compiling both forms against each distro's real headers:

  distro                              (node)      (*node)
  humble                              ok          ok
  jazzy                               ok          ok
  rolling  osrf/ros:rolling-desktop   ok          ok    (tf2_ros 0.46.1)
  rolling  ros-testing on resolute    FAILS       ok    (tf2_ros 0.46.3)

Note the prebuilt rolling desktop image still ships 0.46.1 and therefore
still accepts the old form; only the current ros-testing packages on
Resolute reject it, which is what CI builds against.

Surfaced by the rolling-testing job on #300 once #301 stopped fail-fast
from cancelling it.

Co-authored-by: Claude Opus 5 (1M context) <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.

1 participant