项目:BALL(Biochemical Algorithms Library)
版本:1.5.0-git20220524.d85d2dd
适配目标:HarmonyOS PC / aarch64
适配仓库:OpenHarmonyPCDeveloper/build_in_harmonyos
对应 MR:#6796

说明:本文展示的源码片段均来自本次适配包中的 patches/
conanfile.py 原文。为保证可复核,正文不对源码做示意化改写。

一、这次适配,难点并不只在编译

在这里插入图片描述

BALL 是一个 C++ 生物信息学库。这次适配的版本是
1.5.0-git20220524.d85d2dd,目标环境是 HarmonyOS PC。

一开始处理的是比较典型的移植问题:CMake 配置、Qt 模块、Boost
组件,以及一批老的 C++
接口。把这些问题处理完以后,工程已经能够继续往下构建,但在 Conan 的
test_package 阶段又出现了动态库加载失败。

这个问题最后比前面的编译错误更值得记录。因为它说明,适配一个库时,「能编译」只是其中一个检查点,最终交付给消费者的
package 也需要单独验证。

本次 MR 最终合入了 25 个 patch,主要涉及以下几类修改:

  • 关闭当前环境不需要的 GUI/可选能力,先保证核心库能够构建;
  • 调整 QtXml、Boost.Asio、gzip 等平台差异;
  • 将部分旧 C++ 写法迁移到 C++17;
  • 处理 CMake 4.x 等构建环境变化;
  • 针对最终动态库产物做 install/package 阶段的修正;
  • 补充 consumer 侧验证。

下面按照实际排查顺序展开。

在这里插入图片描述


二、先处理依赖,把 GUI 这条链路拿掉

BALL 上游同时包含核心库和 VIEW/GUI 相关能力,而当前 HarmonyOS PC 环境并没有完整的传统 Linux 桌面 OpenGL 依赖链。

因此这次没有一开始就把 VIEW/桌面 OpenGL 依赖链一起解决,而是先关闭当前环境不需要的能力,把适配范围收敛到核心库:

BALL_HAS_VIEW=OFF
BALL_PYTHON_SUPPORT=OFF
USE_QTWEBENGINE=OFF
USE_TBB=OFF
USE_LPSOLVE=OFF
USE_LIBSVM=OFF
USE_MPI=OFF
USE_FFTWD=OFF
USE_FFTWF=OFF
USE_FFTWL=OFF

这样处理以后,构建目标会集中到 BALL 核心库本身。后面遇到的问题也更容易判断,到底是 BALL 本身的兼容问题,还是 GUI/桌面依赖带来的问题。

Qt 这里也做了同样的处理。上游 CMake 原本直接查找 Qt5 的 Core、Network、Xml。对应的真实 diff 放在下方 CMake 小节中,这里不重复贴代码。

这里需要注意,QtXml 的问题不是简单的「头文件不存在」。当前 HarmonyOS 使用的 qt5-qtbase 包里仍然能够看到部分 QtXml 头文件,但没有可用于最终 find 和链接的 Qt5Xml 库目标。因此继续保留 Qt5::Xml 依赖没有意义。

接下来就需要处理源码里真正使用 QtXml 的地方。


三、QtXml 不可用,源码里的 XML 解析也要跟着调整

检查 BALL 源码后,实际使用 QDomDocument 的地方主要集中在:

  • source/STRUCTURE/assignBondOrderProcessor.C
  • source/STRUCTURE/hybridisationProcessor.C

这两处最终改成了 QtCore 中可以使用的 QXmlStreamReader

assignBondOrderProcessor.C 为例,patch 新增:

+// Qt (OHOS: qt5-qtbase 无 QtXml 模块,用 QtCore 的 QXmlStreamReader)
+#include <QtCore/QXmlStreamReader>
+#include <QtCore/QFile>

解析入口也进行了调整,patch 中对应的实际修改如下:

+		// OHOS 适配:qt5-qtbase 不含 QtXml 模块,改用 QtCore 的 QXmlStreamReader 解析
+		QXmlStreamReader xml(&file);
+		// OHOS: 用 readNext() 显式遍历 token,避免 readNextStartElement()
+		// 在文档 EOF 边界误报 "Premature end of document"(Qt 5.15.2 行为)
+		while (!xml.atEnd())
+		{
+			QXmlStreamReader::TokenType tok = xml.readNext();
+			if (tok == QXmlStreamReader::StartElement && xml.name() == "entry")
+			{
+				pair<String, String> tmp;
+				int start_valence = 0;
+				Position start_idx = 0;
+				vector<int> penalties;
+```

这里还有一个实际排查过程中遇到的小问题:这次没有直接使用
`readNextStartElement()`,而是用 `readNext()` 显式遍历 token。

原因是 Qt 5.15.2 在文档 EOF 边界存在解析行为差异,直接使用
`readNextStartElement()` 时会出现
`「Premature end of document」`。所以这里除了把 API
换掉,还要确认新的解析方式能够覆盖原来的 XML 数据。

`hybridisationProcessor.C`
也做了对应迁移。两处修改的共同点是:不再依赖最终不可用的 QtXml
库,而是使用当前环境仍可用的 QtCore XML 流式解析接口。

------------------------------------------------------------------------

## 四、C++17 兼容:根据编译错误逐处处理

这次构建使用 Clang 15 和 C++17。BALL
的部分历史代码比较老,因此编译时会遇到已被弃用或不符合当前标准的接口。

例如工程里的 C++ 标准从旧版本调整到 C++17:

```diff
-   SET(BALL_PROJECT_COMPILE_FLAGS "${BALL_PROJECT_COMPILE_FLAGS} -std=c++11")
+   SET(BALL_PROJECT_COMPILE_FLAGS "${BALL_PROJECT_COMPILE_FLAGS} -std=c++17")

另一处也从 C++11 调整到 C++17,具体修改见上面的第二个 diff。

同时,部分 STL 适配也需要修改。比如 DNAMutator.C 中:

对应的 deselect 也进行了相同处理:

-       std::for_each(to_optimize_.begin(), to_optimize_.end(), std::mem_fun(&Atom::deselect));
+       std::for_each(to_optimize_.begin(), to_optimize_.end(), std::mem_fn(&Atom::deselect));

这部分修改基本没有涉及业务逻辑,主要是把旧接口替换成当前编译器能够接受的写法。处理方式比较直接:编译器报错到哪里,就沿着具体调用继续检查,而不是一次性对整个工程做大范围重构。


五、Boost 的问题主要是组件和可选能力

Boost 这里没有直接照搬上游的组件列表。原来的配置包含:

chrono
date_time
iostreams
regex
serialization
system
thread

本次 patch 去掉了 regexsystem

源码 4:patches/0003-cmake-BALLConfigBoost.cmake.patch

 SET(BALL_BOOST_COMPONENTS
 	chrono
 	date_time
 	iostreams
-\tregex
 	serialization
-\tsystem
 	thread
 )

除了组件配置之外,Boost.Asio 也是一个实际的兼容点。

当前适配使用的 OHOS Boost 1.91.0.1 包中没有 boost/asio.hpp,而 BALL
的部分网络代码直接依赖 Asio。因此这次没有直接删除整个网络接口,而是通过
BALL_HAS_BOOST_ASIO 做条件编译,并在无 Asio 时保留接口层、让 I/O
显式失败。

没有 Asio 时,相关网络能力进入不可用状态;Socket_test.C
也使用相同的条件控制,在没有 Asio 的情况下仍然让测试目标能够完成链接。

这种处理方式的重点不是「让网络功能看起来还能用」,而是把平台能力边界明确下来。当前环境没有
Asio,就明确认为这部分能力不可用。


六、gzip 也采用同样的处理方式

gzip 的情况比较类似。

BALL 部分文件格式支持 .gz,相关实现依赖 Boost.Iostreams 的 zlib 支持,而当前环境中的 Boost.Iostreams 没有编译 zlib 支持。

因此本次适配没有把 .gz 当作普通文件写入,而是在不支持 gzip 时直接进入错误处理。

genericMolFile.C 中的 patch 原文如下:

+#else
+			// OHOS 适配:boost iostreams 库未编译 zlib 支持,gzip 输出压缩不可用。
+			// 显式报错而非静默降级——禁止把未压缩数据写入 .gz 命名文件
+			// (静默数据格式变更、与上游/跨平台不兼容)。
+			Log.error() << "gzip output compression is disabled on OHOS (boost iostreams built without zlib); "
+			               "refusing to write uncompressed data to '" << zipped_filename_ << "'" << std::endl;
+			compress_output_ = false;
+			zipped_filename_.clear();
+#endif

这里有一个比较重要的原则:不能因为当前平台不支持压缩,就把未压缩数据写进一个 .gz 文件。

这样虽然可能暂时绕过错误,但生成出来的文件格式已经和文件名表达的含义不一致,后续读取时反而更难定位问题。

测试侧也采用条件编译,在没有 gzip 能力的环境中跳过对应专项,而不是修改测试预期。


七、编译通过以后,consumer 暴露了动态库问题

在这里插入图片描述

前面的修改完成后,工程已经可以构建。但运行 Conan test_package 时,仍然出现动态库加载失败:

Error loading shared library libBALL.so.1.5
Error relocating ./test_ball:
BALL::GlobalInitializer::init: symbol not found

刚看到这个报错时,首先怀疑的是动态库依赖不完整。

因此排查是按几个方向逐项进行的:

  1. 检查 DT_NEEDED 是否缺少依赖;
  2. 检查 Boost、Qt5Network、Qt5Core 等依赖是否能够找到;
  3. 验证 LD_LIBRARY_PATH 在当前环境中的实际行为;
  4. 比较 build 目录和 package 目录中的 libBALL.so.1.5
  5. 检查 ELF 的 RUNPATH 在 install 前后是否发生变化。

前几个方向排除以后,问题逐渐集中到了 install 产物。

构建目录里的 libBALL.so.1.5 与 install 后 package 中的文件并不是完全相同的 ELF 状态。install 阶段对动态库进行了处理,原本能够工作的运行时搜索信息发生了变化,最终导致 consumer 加载失败。

这也是这次适配里比较容易被忽略的一点:源码和构建过程都正常,并不代表最终 package 里的 .so 一定正常。


八、最终修复放在 Conan 的 package 阶段

在这里插入图片描述

既然问题出现在最终产物,就没有继续给 consumer 叠加更多运行时环境变量,而是回到 Conan 的 package() 阶段处理。

最终 package() 中相关代码为:

    def package(self):
        cmake = CMake(self)
        cmake.install()
        # E298 修复:上游 install 阶段会改写 libBALL.so.1.5(与构建目录产物
        # md5 不同),OHOS 直跑环境下改写后的 .so 加载失败
        # (Error loading shared library libBALL.so.1.5 / symbol not found)。
        # 用构建目录的原始 .so 覆盖 install 产物,保证与测试通过的构建产物一致。
        import glob as _glob
        import shutil as _shutil
        built = _glob.glob(
            os.path.join(self.build_folder, "**", "libBALL.so.1.5"), recursive=True
        )
        if built:
            _shutil.copy2(built[0], os.path.join(self.package_folder, "lib", "libBALL.so.1.5"))
            # 保持 libBALL.so -> libBALL.so.1.5 符号链接
            _so_link = os.path.join(self.package_folder, "lib", "libBALL.so")
            if os.path.islink(_so_link) or os.path.exists(_so_link):
                os.remove(_so_link)
            os.symlink("libBALL.so.1.5", _so_link)
            self.output.info("E298: 已用构建目录 libBALL.so.1.5 覆盖 install 产物")

这里做的事情比较直接:先正常执行 cmake.install(),然后找到构建目录中已经验证过的 libBALL.so.1.5,覆盖 install 阶段生成的同名文件,并重新建立 libBALL.so 的符号链接。

同时,在 toolchain 中保留链接阶段的 RPATH:

        tc.variables["CMAKE_INSTALL_RPATH_USE_LINK_PATH"] = "ON"

最终 package 侧需要保证两件事情:

  • package 中的动态库与已经验证过的构建产物保持一致;
  • 动态库运行时需要的信息不会在 install 阶段丢失。

九、测试结果:官方测试和 consumer 测试要分开看

在这里插入图片描述

这次最终验证分成两层。

第一层是 BALL 自己的 CTest:

项目 结果


官方测试 286
PASS 284
FAIL 2
通过率 99%

并行执行时曾经出现过几项间歇性失败,逐项串行执行后结果稳定在
284/286。因此这里没有简单地把一次并行执行的失败全部归到代码问题上,而是继续进行了单测重新执行。

剩余两个失败项分别是:

1. FileSystem_test

canonizePath("~/test.dat")~ 展开依赖 HOME 环境变量,HarmonyOS
环境与上游 Linux 的行为存在差异。

2. AssignBondOrderProcessor_test2

USE_FINE_PENALTY 场景下存在浮点精度差异。

AssignBondOrderProcessor_test1
已经通过,因此目前没有证据表明这个失败来自前面的 XML 解析迁移。

这两个测试在 test_package/MANIFEST.yml
中进行了明确豁免,没有通过删除测试或者修改预期值来强行得到全绿结果。


十、为什么还需要 test_package

官方 CTest 和 Conan test_package 验证的其实不是同一件事。

CTest 主要回答:

BALL 自己的测试程序能不能运行?

test_package 回答的是:

一个外部消费者拿到最终 package 后,能不能链接并调用 BALL?

这次 test_package/test.cpp 并不是只检查文件是否存在,而是真正调用 BALL:

BALL::GlobalInitializer* gi = BALL::GlobalInitializer::init();

同时对 BALL 的字符串类型进行了实际操作:

BALL::String s("BALL");
s += "-1.5.0";
s.toUpper();

最后检查结果:

if (s == "BALL-1.5.0")

这也是为什么前面的动态库问题会在 test_package 阶段暴露出来。

从适配验证的角度看,这一层很有必要。否则只跑源码目录里的 CTest,很容易漏掉 install、package 和消费者之间产生的问题。


十一、这次适配留下的几个实际经验

1. 先确定目标,再处理依赖

HarmonyOS PC 适配并不意味着要把传统 Linux 桌面环境全部搬过去。

这次先关闭 VIEW,把问题集中到 BALL 核心库,后面的排查会简单很多。

2. 平台不支持的能力,要明确告诉调用方

Asio 不可用,就让网络能力明确处于不可用状态。

zlib 不可用,就不要生成一个内容实际上没有压缩的 .gz 文件。

对移植项目来说,「明确失败」通常比「看起来成功」更容易维护。

3. 最终 package 也属于适配结果的一部分

这次最典型的问题就是 libBALL.so.1.5

源码可以编译,CMake 可以生成目标,甚至构建目录里的动态库也可以正常使用,但经过
install/package 以后,文件状态发生了变化,consumer 才最终暴露问题。

因此后续做类似的 Conan 适配时,建议把验证链路走完整:

源码修改
   ↓
CMake configure
   ↓
编译
   ↓
官方测试
   ↓
install / package
   ↓
ELF / RUNPATH / DT_NEEDED 检查
   ↓
test_package
   ↓
目标环境实际运行

这条链路不必每次都做得过重,但至少应该确认最终交付给消费者的文件,而不是只确认编译目录里的文件。


十二、总结

BALL 1.5.0-git20220524.d85d2dd 的这次 HarmonyOS PC
适配,前半部分主要是处理平台和编译环境差异,后半部分则转向最终二进制产物。

QtXml、Boost.Asio、gzip 和 C++17
这些问题,分别对应不同层面的兼容处理;而动态库加载问题则说明,Conan
package 本身也需要进入验证范围。

最终结果是:BALL 核心库能够完成 Conan clean build,官方 286 项测试中 284
项通过,剩余 2
项已经确认属于平台环境/浮点精度差异,并在测试清单中做了明确豁免;test_package
也能够从消费者角度完成链接和调用。

这次适配给我最重要的一点经验是:不要把「编译成功」当成适配结束。对于需要通过
Conan 分发的 C++ 库,最终 package
能否被消费者正常加载和使用,同样应该作为验收条件。


十三、后续可复用清单

  • 依赖裁剪原则(二):先关闭当前平台不具备完整依赖链的可选能力,把适配范围收敛到核心库后再排查,避免 GUI/桌面依赖噪声干扰后续判断;裁剪后再逐个确认未通过项究竟来自核心库本身还是平台差异。
  • API 迁移注意事项(三、四):优先把不可用 API 替换为当前环境仍可用的等价接口,并验证新接口在边界场景下的行为;旧 C++ 写法按编译报错逐处迁移到目标标准,避免一次性大范围重构。
  • 平台能力边界处理方式(五、六):能力不可用时通过条件编译保留接口层并让调用方明确失败,而不是静默降级;测试侧同样用条件编译跳过专项,不修改测试预期。
  • Conan package 验证链路(七、八、九、十):把 build 目录、install 产物和最终 package 中的动态库视为不同验证对象,检查 ELF、RUNPATHDT_NEEDED 在 install 前后的变化;官方 CTest 之外还要保留 consumer 侧 test_package,验证外部消费者能否真正链接并调用。
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐