Skip to content

Add Qt 6 support (dual Qt 5 / Qt 6 build) - #1335

Open
drnic wants to merge 18 commits into
ArduPilot:masterfrom
drnic:qt6-support
Open

Add Qt 6 support (dual Qt 5 / Qt 6 build)#1335
drnic wants to merge 18 commits into
ArduPilot:masterfrom
drnic:qt6-support

Conversation

@drnic

@drnic drnic commented Jun 30, 2026

Copy link
Copy Markdown

Summary

Adds Qt 6 support to APM Planner 2.0 while keeping the existing Qt 5 build working — the same source tree compiles and runs on either toolkit (dual build).

Why dual-build rather than a hard cutover? Honestly: because it keeps things low-risk and easy to land. Existing Qt 5 contributors and CI aren't disrupted, and reviewers can adopt Qt 6 at their own pace.

Note for maintainers: if you'd rather not carry both, I'm happy to do a clean cut over to Qt 6 only and drop the Qt 5 compatibility shims — Qt 5 is end-of-life and it's reasonable to leave it behind. Just say the word and I'll follow up with a slimmed-down branch.

Build

Want to try this branch? Clone it and build from source with CMake + Ninja.

git clone https://github.com/drnic/apm_planner.git
cd apm_planner
git checkout qt6-support
mkdir build
cd build

Qt 6 (preferred) — on macOS, install deps with brew install qt cmake ninja sdl2:

cmake -G Ninja .. -DCMAKE_PREFIX_PATH="$(brew --prefix qt);$(brew --prefix)"
ninja

Qt 5 (still supported) — install deps with brew install qt@5 cmake ninja sdl2:

cmake -G Ninja .. -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5);$(brew --prefix)"
ninja

On Linux, cmake -G Ninja .. (without CMAKE_PREFIX_PATH) auto-detects the system Qt. Once built, launch with open apmplanner2.app on macOS or ./apmplanner2 on Linux.

Windows — from a Developer PowerShell for VS (VS 2019/2022 with the C++ workload, plus SDL2 via vcpkg), auto-detect the installed Qt kit instead of hardcoding a version:

$qt = (Get-ChildItem C:\Qt\6.*\msvc*_64 -Directory | Sort-Object Name -Descending | Select-Object -First 1).FullName
cmake -G Ninja .. -DCMAKE_PREFIX_PATH="$qt"
ninja

⚠️ The Windows build is not yet covered by CI (the matrix is Linux + macOS) and hasn't been verified as thoroughly — reports and fixes welcome.

See the README for full per-platform dependency lists.

Methodology

I let Claude Opus 4.8 do this project. For an unmaintained project, our LLM overloads are our best friend. I created a plan for the upgrade, and worked through it.

Github Actions

Travis was dead afaik; so I setup github actions.

image

Testing

I've tested the build + run of the basic app on my MacOS Tahoe 26.

It would be great for others on other OS to build and confirm it is running in the comments.

What changed

  • Build & packaging — qmake (apm_planner.pro) and CMake updated to build against Qt 5 or Qt 6; Debian, RedHat and macOS packaging/deploy scripts updated.
  • CI — replaced .travis.yml with a GitHub Actions workflow (.github/workflows/build.yml).
  • OpenGL — ported CameraView from the removed QGLWidget to QOpenGLWidget.
  • QML — rewrote BarGauge without Qt5-only Controls 1.x; fixed toolbar icons, a phantom map scrollbar, and STATUSTEXT "tofu" boxes under Qt 6.
  • Platform macros — replaced removed Q_OS_MACX with Q_OS_MACOS; dropped the QtMultimedia dependency on macOS.
  • Stability — fixed a Qt 6 startup hang caused by singleton re-entrancy during MainWindow construction.
  • Window title now shows the Qt major version it was built with.
  • Assorted API-compatibility fixes across src/ui, libs/opmapcontrol, and libs/thirdParty/quazip.

drnic added 17 commits June 30, 2026 10:55
Make apm_planner build and link against both Qt5 and Qt6 so the project
can transition off end-of-life Qt5 gradually. Verified: both
`cmake -DCMAKE_PREFIX_PATH=$(brew --prefix qt)` (Qt6) and `qt@5` (Qt5)
configure, build, and produce a working apmplanner2.app.

Build system:
- Root, opmapcontrol, and quazip CMakeLists use the version-agnostic
  `find_package(QT NAMES Qt6 Qt5)` + `Qt${QT_VERSION_MAJOR}` pattern.
- Bump C++ standard 14 -> 17 (required by Qt6, valid for Qt5).
- Qt6 also links Core5Compat (legacy QRegExp/QTextCodec), OpenGLWidgets
  (OpenGL split in Qt6), and SvgWidgets (QGraphicsSvgItem moved there).

Source ports (all dual-safe across Qt5 and Qt6):
- QString methods taking QRegExp (split/replace/contains/indexOf) are
  removed in Qt6 and NOT bridged by Core5Compat -> ported those call
  sites to QRegularExpression; configuration.h now returns
  QRegularExpression. Pure QRegExp-class files just include <QRegExp>.
- toTime_t/fromTime_t/setTime_t -> *SecsSinceEpoch.
- QString::SkipEmptyParts -> Qt::SkipEmptyParts.
- QPainter::HighQualityAntialiasing -> Antialiasing.
- QLayout::setMargin -> setContentsMargins; QFileDialog::DirectoryOnly ->
  Directory; QSortFilterProxyModel::setFilterRegExp ->
  setFilterRegularExpression; qSort -> std::sort; trUtf8 -> tr.
- QTextStream::setCodec guarded -> setEncoding(QStringConverter::Utf8);
  QTextCodec dropped (QString::fromUtf8) / linked via Core5Compat.
- QGLWidget viewport -> QOpenGLWidget + QSurfaceFormat; drop dead
  QGLWidget/QDesktopWidget includes; add explicit QStandardPaths include.
- Explicit QChar() for int->QChar and bool/QString concatenations now
  that QChar(int) is explicit in Qt6; cast qsizetype narrowing.
- Guard `using namespace QtDataVisualization` (namespace gone in Qt6).
- QGC.h: replace isnan/isinf macros (clobbered QtQml's std::isinf) with
  `using std::isnan/isinf`.
- qcustomplot.h: skip the Q_GADGET-in-namespace QCP trick under the
  Qt 6.9+ moc (its enum reflection is unused here).
…struction

On Qt6, adding pages to a QStackedWidget delivers show/hide events
synchronously. During MainWindow construction this fired show/hideEvent
handlers on config pages that call MainWindow::instance()->toolBar(),
but the instance() singleton pointer was not yet assigned, so each call
built another MainWindow and recursed infinitely (app appeared stuck on
the "Starting Communication Links" splash at ~98% CPU).

- MainWindow::instance() now uses a static m_instance member that the
  constructor assigns to `this` as its first action, so re-entrant calls
  during construction return the partially-built window.
- Add MainWindow::isInitialised() (false until the constructor finishes).
- Guard the show/hide handlers in AccelCalibrationConfig,
  ApmCustomFirmwareConfig and Radio3DRConfig to bail out early when the
  main window is not yet initialised.

Also update README build instructions for the Qt6/Qt5 dual build.
Appends [QtN] to the window name so it's obvious at a glance whether a
running build is the Qt5 or Qt6 variant during the dual-build migration.
Two Qt5->Qt6 QML/widget regressions in the Flight Data view:

- Toolbar icons were invisible. The relative image paths in
  ApmToolBar.qml are assigned into the Button component living in
  qml/components/. Qt5 resolved them against ApmToolBar.qml; Qt6
  resolves them against Button.qml, yielding qml/components/resources/...
  which does not exist. Wrap the paths in Qt.resolvedUrl() so they
  resolve in ApmToolBar.qml's context under both Qt versions.

- Button.qml sized the icon with 'width: image.width', where image is
  the source url (url.width is undefined). Qt5 fell back to the implicit
  size; Qt6 turns undefined into 0. Bind to buttonImage.implicitWidth/
  implicitHeight instead.

- OPMapWidget (a QGraphicsView) never set a scrollbar policy, so the
  default ScrollBarAsNeeded let Qt6 show a stray vertical scrollbar on
  resize. The map is drag-to-pan; force both bars off.
STATUSTEXT is a fixed 50-byte buffer the firmware NUL-pads. Building the
QString via QString(const QByteArray&) kept the whole buffer including the
padding, and Qt6 rendered the embedded NULs as .notdef boxes (a grid of
tofu squares over the PFD). Build from the C string instead so the text
stops at the first NUL.
QtQuick.Controls 1.2 and QtQuick.Controls.Styles 1.2 (ProgressBar with
minimumValue/maximumValue/orientation/style: ProgressBarStyle) were
removed in Qt6. Reimplement the vertical bar gauge with plain QtQuick
Rectangle primitives so it builds and runs on both Qt5 and Qt6, with no
QtQuick.Controls dependency. All public properties and the warn/fail
color thresholds are preserved.

Used by the Vibration Monitor and EKF Monitor tool screens.
macOS uses the native SpeechChannel API for audio output, so the
QtMultimedia module is not actually used. Dropping the dependency
and its include avoids requiring QtMultimedia for Qt6 builds.
Qt6 dropped the legacy Q_OS_MACX macro in favor of Q_OS_MACOS.
Update all platform guards to use the current macro so they keep
compiling under Qt6 while remaining valid on Qt5.
QGLWidget and its convertToGLFormat() helper were removed in Qt6. Switch
CameraView to QOpenGLWidget and provide a local convertToGLFormat() that
produces a bottom-up RGBA8888 image for glDrawPixels, working on both Qt5
and Qt6.

QOpenGLWidget only binds its framebuffer/context around paintGL(), so
schedule a repaint via update() instead of calling paintGL() directly.

The fixed-function GL calls (glDrawPixels/glOrtho) need the GL headers
explicitly now, so include them and link OpenGL::GL via CMake on both Qt
versions. Silence the deprecated fixed-function pipeline warnings on macOS.
Complete the dual Qt5/Qt6 migration's build-tooling layer:

- apm_planner.pro: bump c++14 -> c++17 and add a dual-safe guard
  (QT += core5compat openglwidgets svgwidgets) under Qt6, matching the
  Core5Compat bridge and the OpenGL/Svg module splits.
- Delete dead qgcunittest.pro and qgcvideo.pro (referenced removed files
  such as phonon, webkit, SerialLink and the qgcvideo app; broken on Qt5
  too). CMake is the canonical build.
- Packaging moves to Qt6 + CMake: debian/control Qt6 build-deps,
  debian/rules and redhat spec build via cmake, deploy_osx.sh uses the
  Qt6 macdeployqt.
- Replace defunct .travis.yml with a GitHub Actions workflow building a
  {Qt5,Qt6} x {ubuntu,macOS} matrix; release artifact pinned to Qt6.
  Build-only for now: no qgcunittest CMake target exists yet.
The published Qt6 macOS artifact was arm64-only (built on macos-latest)
and shipped as a zip. Build it universal instead:

- Add osx_archs matrix var; the Qt6 macOS leg sets "x86_64;arm64" and
  passes it via CMAKE_OSX_ARCHITECTURES (empty elsewhere: ignored on
  Linux, native on the Qt5/Intel leg). This also fans out to the in-tree
  quazip/alglib/opmapcontrol subprojects.
- Homebrew sdl2 is single-arch and cannot link into a fat binary, so the
  universal leg builds SDL2 from source for both arches and exposes it to
  find_package(SDL2) via CMAKE_PREFIX_PATH.
- Package with macdeployqt -dmg and assert both slices with lipo so a
  thin build fails CI rather than shipping arm64-only.
Qt 6's moc emits TypeAndForceComplete<QCP, ...> for the Q_GADGET-in-a-fake-
class trick, which fails to compile because QCP is really a namespace. This
already affects Qt 6.8, not just 6.9+, so lower the workaround threshold from
6.9.0 to 6.0.0 and skip generating the (unused) QCP gadget metaobject.
qtserialport and qtmultimedia ship in the base Qt5 install and are not
separately-installable aqt modules for 5.15.2, so aqt errored with 'packages
not found'. Drop them from the Qt5 legs, keeping only qtdatavis3d. Also
comment out the macos-13 leg while the other legs are being debugged.
Qt::AA_X11InitThreads was removed in Qt6, so setting it there fails to compile
on the Linux legs. Wrap it in a Qt-version check so it is only applied on Qt5.
AGL.framework was removed from the macOS SDK in Xcode 26, but Qt's macOS CMake
package still lists AGL in Qt::Gui's link interface, so the universal Qt6 build
failed to link with 'ld: framework AGL not found'. Scrub any AGL reference out
of every imported Qt target's INTERFACE_LINK_LIBRARIES.
actions/checkout@v4 and actions/upload-artifact@v4 declare the deprecated
Node.js 20 runtime; bump both to the @v5 majors, which run on Node 24 and
are drop-in for our usage. install-qt-action@v4 already resolves to a
Node 24 build (v4.3.1) so it is left as-is.

Replace the Intel macos-13 Qt5 leg (those runners are queue-starved and no
longer reliably picked up) with a macos-latest Qt5 leg. Qt 5.15.2 has no
arm64 macOS binaries, so it cross-compiles x86_64 on the arm64 runner (the
x86_64 Qt host tools run under Rosetta); it is build-only and ships nothing.
The Qt6 leg stays universal (x86_64;arm64) and remains the shipped .dmg.

Both macOS legs now target an arch set containing x86_64 that Homebrew's
arm64 sdl2 bottle can't satisfy, so SDL2 is always built from source on
macOS (previously gated on the universal leg only).

Add a job-level 30m timeout-minutes as a safety net so a hung leg is killed
promptly (the slowest healthy leg finishes in ~19m) instead of burning the
default 6h of runner time.
@drnic

drnic commented Jul 1, 2026

Copy link
Copy Markdown
Author

Also thanks to claude, github CI is passing:

image

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