HarmonyOS高难适配实践:Actiona 3.11.1构建与X11深度集成降级
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
适配对象: Actiona 3.11.1
目标环境: OpenHarmony / 鸿蒙 PC
适配仓库: OpenHarmonyPCDeveloper/build_in_harmonyos
对应 PR: #5557
对应 Issue: #2239本文记录一次比较典型的桌面软件跨平台适配过程。Actiona 本身是 Qt 项目,但实际运行依赖并不只来自 Qt,还包括 X11、KeySym、CMake 平台条件、第三方 submodule、Qt 配置宏以及 SMTP 的 SSL 能力。
这次适配最终形成了可用于 OpenHarmony PC 构建的 Conan 方案,并有本地 AArch64 产物和 ELF 层面的核验结果。需要特别说明的是:本文不会把没有实际执行的 GUI、完整 X11 runtime、
dlopen/dlsymruntime 或 SMTP TLS 写成“验证通过”。
一、为什么 Actiona 适配起来没有想象中简单
如果只看项目技术栈,Actiona 是一个 Qt 桌面程序,很容易产生一种判断:
“Qt 已经跨平台了,换成鸿蒙编译器应该主要是编译参数的问题。”
实际情况并不是这样。
Actiona 3.11.1 的源码里仍然存在大量 Linux 桌面环境假设,例如:
- X11 Display;
- xkbcommon KeySym;
- X.Org 相关库;
PLATFORM_ID:Linux条件;ExternalProject_Add独立子工程;- Git submodule;
- QtDBus / OpenSSL 配置宏;
- SMTP TLS。
所以这次适配真正需要解决的问题并非“把 Linux 改成 OHOS”,而是先把 Linux 环境中的能力拆开:
Linux
├── X11 Display
├── KeySym
├── X.Org
├── Linux CMake 条件
├── 第三方依赖
└── SSL / TLS
然后逐项判断:
目标环境到底缺的是 Linux 本身,还是 Linux 上的某一项具体能力?
这一步决定了后面应该采用“替换、降级、补依赖”还是“保留”。
图1 Actiona 3.11.1 鸿蒙 PC 适配 PR #5557
本次 PR 标题本身就体现了适配重点:
Actiona 3.11.1 鸿蒙 PC 适配
X11 深度集成降级
test_package 产物验证
但从最终代码来看,真正的工作并不只发生在 C++ 源码里,大量平台处理逻辑是在 Conan recipe 的 source() / generate() / build() 阶段完成的。
二、先把问题拆开,再开始改
这次适配可以归纳成下面几条链路:
这里有一条后面会反复用到的原则:
构建成功、产物存在、符号存在、动态加载成功和实际功能运行,是五件不同的事情。
图 2:鸿蒙 PC HiShell 环境
下面这张图来自实际鸿蒙 PC HiShell 操作环境。可以看到当前目录就是 Actiona 3.11.1 的 Conan 适配目录,同时包含 recipe、patch 和 test_package。

图2 鸿蒙 PC HiShell 环境下的 Actiona 3.11.1 适配目录
从目录结构可以直接看到:
conanfile.py
conandata.yml
manifest.yaml
patches/
test_package/
这也说明这次适配并非单纯修改一个 .cpp 文件,而是同时调整了源码、构建脚本和验证脚本。
三、X11 Display:保留能力,但换掉 Qt Native Interface
3.1 上游实现
Actiona 的 actiontools/src/x11info.cpp 中,原实现通过 Qt X11 Native Interface 获取 Display:
Display *X11Info::display()
{
auto x11App =
qGuiApp->nativeInterface<QNativeInterface::QX11Application>();
return x11App ? x11App->display() : nullptr;
}
这个实现默认存在:
QNativeInterface::QX11Application
而目标环境并不能直接依赖这套 Qt X11 Native Interface。
因此 PR 的 Conan recipe 设计了构建阶段的兼容替换:
Display *X11Info::display()
{
auto x11App = nullptr;
return XOpenDisplay(nullptr); /* 鸿蒙降级 */
}
这里的关键并不是“删除 X11”,而是:
Qt X11 Native Interface
↓
XOpenDisplay
也就是说,底层仍然需要 X11 能力。
3.2 这里还有一个实际踩过的坑
上游文件实际名称是:
actiontools/src/x11info.cpp
而 recipe 中存在:
X11Info.cpp
这种大小写差异。
在大小写敏感文件系统中:
x11info.cpp
X11Info.cpp
不是同一个文件。
因此这部分不能简单写成:
“最终源码已经全部完成 XOpenDisplay 替换。”
更准确的结论是:
PR 中设计了
QX11Application → XOpenDisplay(nullptr)的兼容处理,但现有构建缓存证据不足以证明该文本替换在所有最终构建环境中都实际生效。
这也是 Conan recipe 做源码字符串替换时很容易产生的一种问题:
recipe 执行成功,不等于目标文本一定命中。
四、KeySym:xkbcommon 为什么可以收敛掉
4.1 原始依赖
actiontools/src/keyboardkey_xkb.cpp 原本包含:
#include <xkbcommon/xkbcommon-keysyms.h>
并使用大量:
XKB_KEY_space
XKB_KEY_Return
XKB_KEY_F1
这次适配的思路并非重新实现键盘映射,而是将 KeySym 常量来源换成 X11:
#include <X11/keysym.h>
同时将:
XKB_KEY_*
替换为:
XK_*
如果代码只使用 KeySym 常量,而没有使用 xkbcommon 的运行时 API,那么从依赖角度看,这属于比较典型的依赖收敛。
4.2 修改规模并不小
独立核验上游源码:
XKB_KEY_*出现 107 次;- 去重后有 106 个不同符号;
- 其中有 20 个 XF86 多媒体按键符号。
所以这并非简单修改一两个宏。
4.3 真正容易出错的是 XF86 KeySym
这里是本次适配中最值得单独记录的坑。
普通 KeySym 可以做:
XKB_KEY_space → XK_space
XKB_KEY_Return → XK_Return
XKB_KEY_F1 → XK_F1
但 XF86 键不能简单按照:
0x1008ff00 + index
这样的方式生成。
经独立核验发现,本次涉及的 20 个 XF86 值均存在数值不一致。
例如:
XK_XF86AudioLowerVolume
生成值:0x1008ff00
官方值:0x1008ff11
XK_XF86AudioMute
生成值:0x1008ff02
官方值:0x1008ff12
XK_XF86AudioPlay
生成值:0x1008ff05
官方值:0x1008ff14
因此,这里必须区分:
宏名替换
↓
代码编译
↓
宏数值正确
↓
事件语义正确
↓
实际键盘运行
这些并非同一个验证层级。
目前能确定的是:
20/20 个涉及的 XF86 数值存在与官方定义不一致的情况。
但由于没有 GUI / 键盘运行日志,不能进一步写成“Actiona 的 XF86 功能运行失败”。
五、CMake:不是把 Linux 全部改成 OHOS
这是本次适配中另一个容易误解的地方。
一种看起来很直接的修改方式是:
$<PLATFORM_ID:Linux>
全部改成:
$<PLATFORM_ID:OHOS>
但 PR 实际采取的方式并不是全局替换。
recipe 针对部分
$<PLATFORM_ID:Linux>
条件做定向剥离,同时保留对应源文件;Windows 条件则保持不变:
涉及的典型文件包括:
actiona/CMakeLists.txt
actiontools/CMakeLists.txt
以及:
qhotkey_x11.cpp
keysym2ucs.cpp
keyboardkey_xkb.cpp
qtlockedfile_unix.cpp
x11info.cpp
keysym2ucs.hpp
x11info.hpp
因此更准确的描述是:
只针对需要进入目标构建的 Linux 条目剥离平台条件,而不是把 Linux 全局替换成 OHOS。
这个区别很重要。
因为平台宏表达的有时不是“操作系统”,而是:
某项底层能力是否存在。
六、ExternalProject:主工程找到 Qt,不代表子工程也能找到
Actiontools 的使用:
ExternalProject_Add(...)
这类结构在跨平台适配中很容易被遗漏。
主工程:
CMake
↓
find_package(Qt6)
成功,并不能说明 ExternalProject 启动的新 CMake 进程也能找到 Qt。
因此 PR 增加了 patch,将 Qt6 配置目录显式传给子工程:
ExternalProject_Add(external_qtjsapi
...
CMAKE_ARGS
-DQt6_DIR=${Qt6_DIR}
-DQt6Qml_DIR=${Qt6Qml_DIR}
-DQt6UiTools_DIR=${Qt6UiTools_DIR}
-DQt6Core5Compat_DIR=${Qt6Core5Compat_DIR}
-DQt6Widgets_DIR=${Qt6Widgets_DIR}
-DQt6Gui_DIR=${Qt6Gui_DIR}
)
问题可以表示为:
主工程 CMake
│
├── Qt6
│
└── ExternalProject
│
└── 独立 CMake
│
└── 也必须知道 Qt6_DIR
这是本次 PR 中非常值得借鉴的一条经验:
遇到 ExternalProject,不要默认它继承主工程所有构建变量。
七、第三方依赖:Git 仓库能编译,不代表 release tarball 能编译
Actiontools 上游有多个 Git submodule,例如:
mINI
gui breakpad
actiona breakpad
QDarkStyleSheet
qtjsapi
但 release tarball 中这些 submodule 目录可能只有空目录。
所以 Conan recipe 又单独下载了:
qtjsapi
QDarkStyleSheet
mINI
并使用:
固定 commit
+
SHA256
来保证依赖可重复恢复。
这条链路可以概括成:
release tarball
↓
发现 submodule 源码为空
↓
Conan recipe 下载
↓
固定 commit
↓
SHA256 校验
↓
恢复源码树
↓
进入 CMake
所以做包管理适配时要特别注意:
Git 工作区 ≠ release 源码包。
本地 clone 能编译,并不能直接证明 Conan recipe 从干净源码包也能复现。
八、Qt 配置宏:临时绕过和真正支持不是一回事
Issue #2239 中涉及:
QT_NO_DBUS
QT_NO_OPENSSL
QT_NO_SSL
这些宏会直接影响 QtDBus、OpenSSL 和 SSL 相关代码的条件编译。
对于非变体 qtbase,recipe 存在类似这样的处理路径:
备份 qconfig.h
↓
临时处理相关宏
↓
完成 Actiona 构建
↓
finally 恢复
这种做法的价值在于:
不把临时适配修改永久写进共享 Qt 缓存。
但这里也需要留边界。
不能概括成:
“所有 Qt 包都可以自动恢复。”
核验结果表明,qtbase-sql-dbus 变体的处理路径并不完全相同。
所以更准确的说法是:
非变体 qtbase 存在备份—临时修改—恢复处理,其他 Qt 变体仍需分别确认。
九、SSL:这里不是“换个类”,而是功能降级
SMTP 部分是本次适配中最容易被一句话带过的地方。
原实现使用:
#ifndef QT_NO_OPENSSL
qxt_d().socket = new QSslSocket(this);
#else
qxt_d().socket = new QTcpSocket(this);
#endif
同时存在:
connectToHostEncrypted(...)
以及:
socket->startClientEncryption();
本次 recipe 将相关实现向普通 TCP 收敛:
QSslSocket
↓
QTcpSocket
以及:
connectToHostEncrypted()
↓
connectToHost()
因此能力变化非常明确:
| 能力 | 原实现 | 当前方案 |
|---|---|---|
| TCP 建连 | 支持 | 支持 |
| SMTP 状态机 | 支持 | 代码路径保留 |
| TLS 握手 | 支持 | 不支持 |
| 数据加密 | 支持 | 不支持 |
| 证书验证 | 支持 | 不支持 |
| TLS 传输保护 | 支持 | 不具备 |
还有一个容易忽略的细节:
调用函数仍然叫:
connectToSecureHost(...)
但内部连接已经可能变成:
connectToHost(...)
所以接口命名与实际传输能力已经出现语义不一致。
因此这部分必须定义为:
SMTP 通信从 TLS 加密连接降级为普通 TCP 连接。
而不是:
“SSL 适配完成。”
如果后续要用于生产环境邮件通信,仍需要重新解决 OpenSSL、Qt SSL、证书验证和 TLS 握手问题。
十、PR 踩坑:几个容易“看起来已经完成”的地方
10.1 recipe 执行了,不代表源码替换命中了
这次大量修改是通过 Python 字符串/正则完成的。
这种方式很适合维护 Conan recipe,但它不是 AST 级语义修改。
例如:
x11info.cpp
X11Info.cpp
这种大小写差异就可能导致替换目标无法命中。
因此,以后遇到类似方案,建议形成固定动作:
recipe 修改
↓
重新生成源码
↓
直接 grep / sed 检查目标文件
↓
确认替换前后内容
↓
再继续编译
不要只看 Conan 日志里有没有执行对应 Python 分支。
10.2 ExternalProject 是第二套 CMake
只要看到:
ExternalProject_Add(...)
就应该立即检查:
- Qt6_DIR;
- CMAKE_PREFIX_PATH;
- 编译器;
- sysroot;
- 子工程依赖。
否则很容易出现:
主工程正常,子工程失败。
10.3 test_package 有代码,不等于 runtime test 跑过
test_package/test.c 中确实存在:
dlopen(..., RTLD_NOW);
以及:
dlsym(...);
还会调用:
keysym2ucs
但是当前 conanfile.py::test() 并没有调用:
cmake.test()
同时 test_package/CMakeLists.txt 也没有看到:
enable_testing()
add_test(...)
所以:
测试代码存在
↓
测试程序能够构建
↓
测试被注册
↓
测试被执行
↓
测试通过
是完全不同的五个状态。
当前证据能够确认的是:
runtime 测试代码已经写出,但没有真正接入当前执行链。
10.4 ELF 依赖路径也值得继续验证
本地检查中,actiontools.so 存在:
DT_NEEDED: ../tools/tools.so
而 package 中对应文件是:
lib/actiona/actiontools.so
lib/actiona/tools.so
因此后续真正执行:
dlopen(..., RTLD_NOW)
时,还需要继续确认动态库依赖路径是否能够正确解析。
这里不直接下结论说“已经加载失败”,因为当前 runtime test 本身并未执行。
更严谨的结论是:
runtime 尚未实际验证,同时 ELF 依赖路径和 package 目录布局之间存在值得继续检查的地方。
10.5 CI 失败不能写成 CI 通过
PR 最后一次 CI 构建在 L0 的 registry 可达性阶段出现:
registry_unreachable
后续部分构建阶段没有继续执行。
所以:
PR 已合并
≠
CI 完整构建通过
这也是适配复盘里非常值得保留的一条经验:
基础设施失败和代码失败要分开记录。
十一、产物级验证:从“编译成功”继续往下走
传统适配文章经常在这里停下:
“编译成功,适配完成。”
但真正有意义的验证至少应该继续往下:
Build
↓
Package
↓
Artifact
↓
ELF
↓
Symbol
↓
Dynamic Load
↓
Runtime
↓
Feature
11.1 产物存在性
当前本地 Conan cache 中能够看到:
bin/actiona
lib/actiona/actiontools.so
lib/actiona/tools.so
这说明目标产物已经生成。
但:
文件存在 ≠ 程序能够正常启动。
11.2 ELF 类型
可以用:
file path/to/actiona
file path/to/actiontools.so
file path/to/tools.so
确认最终产物是否为目标架构 ELF。
当前本地证据中:
actiona ELF64 AArch64 PIE
actiontools.so ELF64 AArch64
tools.so ELF64 AArch64
这比“编译命令没有报错”更接近最终交付物。
11.3 检查关键符号
例如:
readelf -Ws path/to/actiontools.so | grep keysym2ucs
这一步回答的是:
目标符号是否真正进入最终 ELF?
独立检查确认了相关符号存在,并进一步核验了 keysym2ucs 对应的函数符号属性。
但它仍然不能证明:
dlopen
一定成功。
11.4 Runtime:目前不要过度解读
test_package/test.c 的设计目标其实已经很明确:
dlopen
↓
dlsym
↓
keysym2ucs
↓
实际函数调用
但当前 test() 没有调用 cmake.test()。
因此本文最终只把它记录为:
已经设计 runtime 测试,但当前证据不足以证明 runtime 测试实际执行。
这比简单写“测试通过”更准确,也更便于后续继续补充验证。
十二、验证结果矩阵
| 验证项 | 当前状态 | 能证明什么 |
|---|---|---|
| PR/recipe | 已确认 | 适配方案已提交 |
| Conan 构建 | 有本地证据 | 构建链可产生目标产物 |
bin/actiona | 已确认 | 目标可执行文件存在 |
actiontools.so | 已确认 | 目标动态库存在 |
tools.so | 已确认 | 目标动态库存在 |
| AArch64 ELF | 已确认 | 产物架构正确 |
readelf | 已确认 | 关键符号进入 ELF |
dlopen() | 未确认 | runtime 未实际执行 |
dlsym() | 未确认 | runtime 未实际执行 |
| GUI | 未确认 | 没有充分 GUI 运行证据 |
| X11 runtime | 未确认 | 没有完整 X11 运行证据 |
| KeySym | 有源码替换 | XF86 数值存在风险 |
| SMTP TCP | 代码级降级 | TLS 路径改为普通 TCP |
| SMTP TLS | 不具备 | 不应描述为 TLS 支持 |
| CI 完整通过 | 未确认 | 最后一次受 registry 可达性影响 |
这张表是本文最重要的结论之一。
十三、如果要自己复现,建议按这个顺序检查
13.1 编译器
which clang++
clang++ --version
目标环境使用:
clang 15.0.4
aarch64-unknown-linux-ohos
13.2 CMake
which cmake
cmake --version
13.3 Conan
conan --version
conan profile show
重点确认 Host profile:
arch=armv8
build_type=Release
compiler=clang
compiler.version=15
compiler.cppstd=gnu14
compiler.libcxx=libstdc++11
os=OHOS
os.version=6.0
13.4 构建
在 Actiona 3.11.1 recipe 对应目录执行:
conan create .
如果构建过程中出现依赖或 registry 问题,先区分:
代码问题
还是:
依赖 / registry / 构建基础设施问题
不要混为一谈。
图3 Actiona 3.11.1 本地 Conan 构建过程
13.5 查找产物
find . -name actiona -o -name actiontools.so -o -name tools.so
如果当前目录找不到,不要继续在源码目录里盲目搜索。
Conan 包产物通常位于 Conan cache 中,应结合:
conan cache path <package>
或直接查看对应 package cache。
13.6 检查 ELF
file path/to/actiona
file path/to/actiontools.so
file path/to/tools.so
13.7 检查关键符号
readelf -Ws path/to/actiontools.so | grep keysym2ucs
十四、这次适配真正留下来的几个经验
14.1 不要把 Linux 当成一个整体
跨平台适配时,最先要拆开的不是代码,而是平台假设。
Linux
├── X11
├── KeySym
├── X.Org
├── CMake 条件
├── DBus
└── SSL
这些能力应该分别处理。
14.2 依赖降级和能力降级不是一回事
例如:
xkbcommon
↓
X11 KeySym
更多属于依赖收敛。
而:
QSslSocket + TLS
↓
QTcpSocket + TCP
是真正的能力降级。
还有:
qconfig.h
↓
临时修改
↓
恢复
属于构建配置层面的规避。
这三类修改,在后续维护时不应混在一起。
14.3 “有测试代码”也不能等价于“测试通过”
这次 test_package 的情况非常典型:
测试代码存在
≠
测试程序执行
≠
runtime 验证通过
以后写适配复盘,建议把验证链明确写出来。
14.4 最终交付应该看产物,而不只是看编译日志
一个更可靠的闭环是:
源码
↓
构建
↓
Package
↓
ELF
↓
Symbol
↓
Dynamic Load
↓
Runtime
↓
Feature
每往下一层,证明的东西更多。
所以“编译成功”只是中间结果,不是最终结论。
十五、总结
Actiona 3.11.1 的鸿蒙 PC 适配,真正耗时的并不是修改几个 #ifdef。
从源码到构建链,实际涉及:
Qt Native Interface
↓
X11 Display
↓
xkbcommon / KeySym
↓
CMake Platform
↓
ExternalProject
↓
Qt6 Config
↓
Git submodule
↓
Conan recipe
↓
Qt 配置宏
↓
SMTP SSL
↓
Package / ELF
↓
Runtime Test
这些问题看起来都属于“平台适配”,但实际性质不同:
- 有的是接口替换;
- 有的是依赖收敛;
- 有的是构建条件调整;
- 有的是第三方依赖补齐;
- 有的是配置规避;
- 有的是明确的功能降级。
因此这次复盘最后得到的结论并不是一句“Actiona 已经完全鸿蒙化”。
更准确地说:
Actiona 3.11.1 已形成面向 OpenHarmony / 鸿蒙 PC 的 Conan 适配方案,并存在本地 AArch64 产物和 ELF 层面的核验结果;但 GUI、完整 X11 runtime、
dlopen/dlsymruntime 以及 SMTP TLS 仍不能从现有证据中确认。
对于后续类似的第三方软件适配,最值得复用的还是三个问题:
- 这个工程依赖的到底是 Linux,还是 Linux 上的某项具体能力?
- 主工程、ExternalProject 和第三方依赖是不是处在不同的构建环境?
- 当前验证到底停在 Build、Package、Symbol,还是已经真正进入 Runtime?
跨平台适配真正难的地方,往往不是让代码通过编译,而是把原有平台假设拆开以后,准确知道:
哪些能力保住了,哪些能力换了实现,哪些能力只是暂时绕过去,哪些能力已经明确降级,以及哪些事情其实还没有验证。
参考资料
-
OpenHarmonyPCDeveloper/build_in_harmonyos
https://gitcode.com/OpenHarmonyPCDeveloper/build_in_harmonyos -
Actiona 3.11.1 鸿蒙 PC 适配 PR #5557
https://gitcode.com/OpenHarmonyPCDeveloper/build_in_harmonyos/merge_requests/5557 -
Actiona 3.11.1 鸿蒙 PC 适配 Issue #2239
https://gitcode.com/OpenHarmonyPCDeveloper/build_in_harmonyos/issues/2239 -
Actiona 项目
https://github.com/Jmgr/actiona

更多推荐



所有评论(0)