fix: build bundled fcitx5 plugin against the bundled Qt#5142
Conversation
The fcitx5 plugin subclasses QPlatformInputContext, a Qt private API with no ABI guarantee across minor versions. CI built it against Qt 6.2.4 but shipped it inside PyQt6's Qt 6.11.0, so it SIGSEGVs on startup for every fcitx5 user. Build the plugin against the same Qt as the bundled pyqt6-qt6, and install it where the packaging step looks: - Fetch that Qt as qt_prefix (CMAKE_PREFIX_PATH); install to the qt6/plugins path qt/tools/build_installer.py scans (CMAKE_INSTALL_QT6PLUGINDIR). - QT_FIND_PRIVATE_MODULES=ON — Qt 6.11 won't expose the private includes without it. - Drop qt6-base-dev / qt6-base-private-dev so find_package(Qt6) can't silently fall back to the distro Qt (that would rebuild the bug with CI still green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| gettext \ | ||
| patchelf | ||
| # Build against the same Qt as the bundled pyqt6-qt6 | ||
| qt_version=$(grep 'name = "pyqt6-qt6"' uv.lock \ |
There was a problem hiding this comment.
Haven't tested yet, but a small nitpick: doubt we can rely on uv.lock's ordering here, maybe try parsing uv export --package aqt --extra qt's output instead?
There was a problem hiding this comment.
Good catch. Checked, and uv.lock has four pyqt6-qt6 entries — 6.11.0, 6.10.2,
6.8.1, 6.9.1 (one per extra). The original grep was picking one by ordering alone.
Switched to your suggestion:
qt_version=$(uv export --frozen --no-hashes --package aqt --extra qt \
| grep -oP '^pyqt6-qt6==\K\S+')Pushed as a follow-up commit. Thanks!
There was a problem hiding this comment.
Having re-read the linked forum post, building against qt6.2 doesn't seem to be the cause here?
There was a problem hiding this comment.
The thread establishes that deleting the .so stops the crash — so the plugin is what's crashing. It never gets to why; it stops at the workaround. So it isn't evidence against a Qt-version cause, it just never reaches one. Here's what I found when I went looking.
Qt's BC guarantee doesn't cover this plugin
There are no source or binary compatibility guarantees for the QPA classes, meaning that the API is only guaranteed to work with the Qt version it was developed against.
and the header itself, qtbase/src/gui/kernel/qplatforminputcontext.h:
This file is part of the QPA API ... Usage of this API may make your code source and binary incompatible with future versions of Qt.
Qt even installs it under a version-stamped private include dir — include/QtGui/6.11.0/QtGui/qpa/, reachable only via Qt6::GuiPrivate — which is why this PR has to pass -DQT_FIND_PRIVATE_MODULES=ON to compile at all.
fcitx5-qt is built directly on it — qt5/platforminputcontext/qfcitxplatforminputcontext.h (the qt6/ copy is a symlink to it):
22: #include <qpa/qplatforminputcontext.h>
143: class QFcitxPlatformInputContext : public QPlatformInputContext {
216: FcitxQtWatcher *watcher_; // first data memberWhat actually breaks
qtbase d9bb8c0a17, "Automatically reflect new input context input direction when locale changes", first released in 6.7.0, adds both halves of this:
// qplatforminputcontext.h
friend class QInputMethod;
+
+ Qt::LayoutDirection m_inputDirection;
// qplatforminputcontext.cpp
QPlatformInputContext::QPlatformInputContext()
: QObject(*(new QPlatformInputContextPrivate))
{
+ // Delay initialization of cached input direction
+ // until super class has finished constructing.
+ QMetaObject::invokeMethod(this, [this]{
+ m_inputDirection = inputDirection();
+ }, Qt::QueuedConnection);
}That member takes sizeof(QPlatformInputContext) from 16 to 24.
The plugin is compiled against the 6.2 headers, where the base is still 16 bytes — so QFcitxPlatformInputContext's first data member, watcher_, lands at offset 16. In the 6.11 QtGui it is actually running against, offset 16 is m_inputDirection. The two overlap. And because the write is queued, it lands after the plugin's constructor has already set watcher_.
The result: QObject::connect(fcitx::FcitxQtWatcher, fcitx::FcitxQtInputContextProxy): invalid nullptr parameter, then SIGSEGV at 0x10. No keystroke needed — and that constructor is the frame in the forum backtrace.
There's a second, independent break, straight from the shipped 26.05 tarball:
$ cd anki-26.05-linux-x86_64/app_packages/PyQt6/Qt6
# what the bundled plugin (ELF tag Qt_6.2) needs:
$ nm -DC plugins/platforminputcontexts/libfcitx5platforminputcontextplugin.so | grep handleExtendedKeyEvent
U QWindowSystemInterface::handleExtendedKeyEvent(..., QString const&, bool, unsigned short, bool)@Qt_6_PRIVATE_API
# what the Qt sitting next to it (6.11.0) provides:
$ nm -DC lib/libQt6Gui.so.6 | grep handleExtendedKeyEvent
0000000000264510 T QWindowSystemInterface::handleExtendedKeyEvent(..., QString const&, bool, unsigned short)@@Qt_6_PRIVATE_APIBoth files come out of the same installer. The trailing bool was dropped after 6.2, so the symbol the shipped plugin imports does not exist in the Qt it is shipped with. It gets past dlopen only because the plugin isn't BIND_NOW.
Reproduction
On a machine running fcitx5 with no fcitx4 installed at all — so the fcitx4 angle raised in the thread isn't in play — QT_IM_MODULE=fcitx, which is what makes Qt load this plugin at all → SIGSEGV; unset, the plugin isn't loaded and there's no crash, the same effect as deleting it.
Rebuilding the same fcitx5-qt 5.0.10 source against 6.11, with nothing else changed → no crash, and CJK input works.
uv.lock carries four pyqt6-qt6 specifiers, one per qt/qt68/qt69/qt610 extra; picking the first one relied on an ordering uv never promised. Ask uv for the qt extra directly — the same one the build resolves (build/configure/src/python.rs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # CONTRIBUTORS
Documentation build overview
4 files changed± genindex.html± autoapi/anki/collection/index.html± autoapi/aqt/mediasrv/index.html± autoapi/aqt/webview/index.html |
|
Thank you. Let's give it a test in the next beta. Can you make a change to the CONTRIBUTORS file from the GitHub UI to fix the CI check? |
|
Done — updated it via the web UI, thanks! |
Linked issue (required)
Fixes #5110
Summary / motivation (required)
CI builds the fcitx5 plugin against one Qt but ships it inside PyQt6's Qt — a
version mismatch that crashes Anki on startup for every fcitx5 user.
Root cause: the plugin subclasses
QPlatformInputContext, a Qt private APIwith no ABI guarantee across minor versions. Built against Qt 6.2.4 headers but
run inside Qt 6.11.0, it dereferences a misplaced member and SIGSEGVs on first
focus.
Solution: build the plugin against the same Qt as the bundled
pyqt6-qt6and install it where the packaging step (
qt/tools/build_installer.py) looks.The per-flag reasoning is in the commit message.
Steps to reproduce (required, use N/A if not applicable)
official Anki 26.05 release.
How to test (required)
Checklist (minimum)
./ninja checkor an equivalent relevant check locally.Details
release.ymlisworkflow_dispatch-only, so it can't run on the PR. Ireproduced its build step locally: rebuilt the plugin against Qt 6.11.0
(confirmed the
Qt_6.11ELF tag and a cleandlopen(RTLD_NOW)), dropped it intoan Anki 26.05 bundle, and typed Chinese and Japanese without the crash.
Risk / compatibility / migration (optional)
build-linux-x86only.build-linux-arm-installerinstalls a prebuiltfcitx5-frontend-qt6from apt, built against Ubuntu's Qt rather than thebundled 6.11.0 — the same class of mismatch. I have no arm hardware to confirm
or fix it, so it's left for a follow-up.
aqtinstallstep (~1.5 GB Qt download) to the Linux release build.release.ymlisworkflow_dispatch-only, so anexternal contributor can't run it; only its build step is reproduced locally
(see How to test). Please confirm it in a real release build.
UI evidence (required for visual changes; otherwise N/A)
N/A
Scope