鸿蒙PC移植OpenBabel3.2.1:跨DSO修复实录
欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
| 项 | 值 |
|---|---|
| 对象 | OpenBabel 3.2.1(化学信息学工具箱:127 种化学格式互转 / 分子描述符 / 力场) |
| 目标平台 | 鸿蒙 PC(HUAWEI HAD-W32,HarmonyOS 6.1.0,aarch64) |
| 配方仓库 | AtomGit:OpenHarmonyPCDeveloper/build_in_harmonyos |
| PR | #14369 |
| 制品仓 | ohpcd(recipe revision efe11bdc6fc08ae73e08616a5762e592,2026-10-13 发布) |
摘要:本文记录化学信息学基础库 OpenBabel 3.2.1 适配鸿蒙 PC(aarch64)的完整过程。上游是多 DSO 插件架构,127 个化学格式插件运行时 dlopen 加载,ABI 与构建链差异让这些插件与主库里的"同一个注册表"各持一份私有副本,全部格式转换失效,跨 DSO 的 dynamic_cast 还会落进空指针陷阱。最终用 10 个平台无关补丁(插件注册表单实例化 8 个 + 跨 DSO typeinfo 唯一性 2 个)加 2 个工具链级修复(SONAME 旗标注入、CMake 4.1.2 安装目录相对值陷阱)和 1 个 sysroot 共享 zlib 注入方案收口:上游 ctest 348 项全量实跑 348/348 通过(99.81 秒),消费者侧硬断言 4/4,149 个 ELF 全部签名,PR #14369 合入 main 并发布到 ohpcd 制品仓,4 条知识条目正式入档(E1174/E1179/E1202/E1216)。
定性开场:OpenBabel 的格式转换能力分散在 127 个独立加载的 .so 插件文件里,鸿蒙 PC 环境下这些"看似相同"的插件注册表其实是每个 DSO 的私有副本;修复点全部在 ABI 层面,没有一行配置能替代。
声明:
- 348 项上游测试全部在本鸿蒙 PC aarch64 真机原生执行(与 CI 环境同构:clang 15 / ohos-sdk_26.0.0.18);通过 = “本平台行为与上游期望一致”,不构成对化学格式语义正确性的再认证。
- 消费者测试为 L2 级(真实 API + 结果断言),覆盖 SMILES→InChI 核心链路;其余 125+ 种格式转换正确性由上游 348 项套件(含逐格式 fuzz 回归)保障,未在本机逐一重跑。
目录
- 背景:为什么值得做
- 环境与差异前置
- 适配过程(8 个坑,四段式)
- 验证(4.1 上游实跑 / 4.2 消费者 / 4.3 产物 / 4.4 CI 与评审 / 4.5 复现速查)
- 知识沉淀
- FAQ
- 总结
一、背景:为什么值得做
生态位(第一层):OpenBabel 是化学信息学的基础库,提供 127 种化学格式(SMILES、SDF、PDB、MOL、InChI、CML、JSON 等)的双向互转,以及分子描述符、指纹、力场等计算能力,是化学/生物工具链中的"格式枢纽"。鸿蒙 PC 侧目前没有化学信息学生态,适配它属于补生态空白——化学计算、分子可视化、材料数据管线这类工具链的后续适配都会踩到它。
难度代表性(第二层):上游构建脚本停留在 CMake 2.8.12 时代,多 DSO 插件架构是"老库 + 新平台 + 跨 DSO ABI"的典型样本。适配它一次,沉淀的是可复用的方法集(注册表单实例化、模板 typeinfo 收敛、工具链旗标补全、configure 期环境预置),对同类老库适配有直接参考价值。
可行性(第三层):上游自带完整测试体系(348 项 ctest + 36,814 个测试数据文件 + fuzz 回归),“带验证地适配”(适配完必须能跑上游测试)的前提成立;依赖链短(制品仓 3 个包 + sysroot zlib + 源码内置 inchi),工作量可控。
二、环境与差异前置
2.1 环境表(实测值,2026-10-18 复核)
| 项 | 值 | 测量命令 |
|---|---|---|
| 设备 | HUAWEI HAD-W32 | param get const.product.model |
| 系统 | HarmonyOS 6.1.0(build 115) | uname -s(HarmonyOS);param get const.ohos.fullname(OpenHarmony-6.1.0.115) |
| 架构 | aarch64 | uname -m |
| 编译器 | clang/clang++ 15.0.4 | /data/service/hnp/bin/clang --version |
| 构建工具 | CMake 4.1.2 / ctest 4.1.2 | cmake --version / ctest --version |
| 构建系统 | Conan 2.29.1(OHOS 适配版) | conan --version |
| OHOS SDK | ohos-sdk_26.0.0.18 | ls /data/service/hnp/ohos-sdk.org/ |
| Conan profile | os=OHOS, os.version=6.0, arch=armv8, compiler=clang 15, libcxx=libc++, cppstd=17, build_type=Release | 仓库 ci/conan/profiles/ohos-aarch64 |
| 上游源 | https://github.com/openbabel/openbabel/archive/refs/tags/openbabel-3-2-1.tar.gz(实测 HTTP 200) | conandata.yml + curl -sI -L |
| SHA-256 | e140c25480fe1678d00b9a52462368fa4e7805fba67b12ee496784437f3e239e | conandata.yml(conan 下载时自动校验) |
2.2 差异点全表(先列全,第三节逐个展开)
| # | 差异 | 上游假设 | 鸿蒙 PC 实际情况 |
|---|---|---|---|
| D1 | 多 DSO 插件注册表 | 单进程单注册表(上游为 Cygwin/MinGW 的同一问题已备好外置定义正解,但被平台条件锁死) | 默认分支 Map() 是头内 inline 的 function-local static:每个 DSO(主库 / 127 插件 / 测试可执行文件)各持一份私有副本,注册与查找落进不同表 |
| D2 | 跨 DSO 模板 typeinfo | 同名模板实例化跨 DSO 的 dynamic_cast 可命中 | OBPairTemplate<int/double/bool> 在多个 DSO 各自实例化出 WEAK typeinfo/vtable;Itanium ABI 的 operator== 只比指针 → 跨 DSO dynamic_cast 恒失败,紧随其后空指针解引用 |
| D3 | SONAME | 工具链默认给共享库设 SONAME,DT_NEEDED 记库名 | OHOS 工具链默认不加 -Wl,-soname;无 SONAME 时链接器把依赖库相对路径写进消费者 DT_NEEDED,dlopen 按进程 CWD 相对解析而失败 |
| D4 | 构建树库位置 | CMake if(UNIX) 为真,库放 <build>/lib | CMAKE_SYSTEM_NAME=HarmonyOS → if(UNIX) 为假 → 构建树库落 <build>/bin;测试 ENVIRONMENT 烘焙的是 <build>/lib → 错位 |
| D5 | 安装目录变量 | -D 传相对值有效 | CMake 4.1.2:UNINITIALIZED 相对值在上游 set(CACHE PATH) 类型升级时解析到 CMAKE_BINARY_DIR → 安装目的地=构建树,cmake --install 静默跳过主库 |
| D6 | 测试环境 | LD_LIBRARY_PATH 在 configure 后不变 | CTestTestfile.cmake 的 $ENV{...} 在 configure 期求值后定死;Conan 流程只能改 configure 之前的环境 |
| D7 | 插件搜索路径 | 编译期 OB_MODULE_PATH(<prefix>/lib/openbabel/3.2.1)构建期有效 | 该目录 install 后才有;build() 阶段 ctest 只能用 BABEL_LIBDIR 指向构建树 |
| D8 | ELF 执行 | 构建出的 .so/可执行文件可直接 dlopen/执行 | OHOS 内核拒绝执行/dlopen 未签名 ELF → 149 个产物须就地自签名 |
| D9 | ctest 查找 | ctest 在 PATH 中 | conan 进程的 PATH 不含 profile buildenv(仅注入 self.run 的 shell 环境)→ 需显式解析 |
| D10 | zlib | find_package(ZLIB) 可选,任意 zlib 可链 | 制品仓 conan zlib/1.3.1 为静态非 -fPIC,链入共享库触发 aarch64 重定位错误 → 用 sysroot 共享 libz.so |
| D11 | libxml2/rapidjson 探测 | module 模式 find_package(LibXml2) 判 LIBXML2_FOUND;RAPIDJSON_VERSION 缺失可接受(外拉) | CMake 3.24+ PREFER_CONFIG 被 conan 生成的 Config 文件抢占(只置 LibXml2_FOUND);外拉 github 网络不稳 → cmake_find_mode=none + 确定性变量注入 |
| D12 | CMake 版本 | cmake_minimum_required(2.8.12) | CMake 4.x 已移除 < 3.5 兼容 → 需 CMAKE_POLICY_VERSION_MINIMUM=3.5 |
D1–D9 在第三节逐坑展开(按适配笔记的遭遇顺序记录,非补丁编号顺序);D10–D12 属于计划性适配——首次编译前读 CMakeLists 侦察即可识别,走"侦察→配置"而非"失败→修复"循环,见 3.0 依赖树与关键选项。
三、适配过程
3.0 侦察与依赖树
适配前准备三流程(W0 依赖发布检查 + 构建系统侦察 + R51 测试清点):
- 依赖发布检查:
conan download <pkg>/<ver> -r=ohpcd --only-recipe逐个探测 eigen/3.4.1、libxml2/2.15.3.1、rapidjson/1.1.0 → 全部命中,可开工(缺失则须先适配发布依赖,再回来构建下游)。 - 构建系统侦察:CMake(
cmake_minimum_required(2.8.12),2014 年时代的脚本),插件以 MODULE 方式构建、运行时 dlopen 加载。 - 测试清点(R51 Step 1):上游测试在
test/(单数)目录,用例由test/CMakeLists.txt的双重 foreach 动态注册(cpptests×parts)加 origtests/inchi/fuzz 回归块;ctest -N实测总用例数 = 348;测试数据 =test/36,814 个文件(pdb_ligands_sdf 36,168 + files 419 + 其余子目录 227)+ 源码根data/65 个(BABEL_DATADIR,测试 ENVIRONMENT 已烘焙)。
两个战略决策:
- 共享构建(
BUILD_SHARED=ON):仅共享模式构建 5 个插件组 + 全部格式插件(MODULE + dlopen);静态构建下插件根本不编译,348 项 ctest 不可运行——共享构建才是完整形态。 - zlib 走 sysroot(D10):制品仓 conan zlib/1.3.1 为静态非 -fPIC,链入共享库触发 aarch64 重定位错误;改用 OHOS SDK sysroot 自带共享 libz.so(
/data/service/hnp/ohos-sdk.org/<sdk>/ohos/native/sysroot/usr/lib/aarch64-linux-ohos/libz.so),配方自动探测最新 SDK。
openbabel/3.2.1 (BUILD_SHARED=ON, CMake 4.1.2)
├── eigen/3.4.1 制品仓 | 力场 L-BFGS,Eigen3Config.cmake CONFIG 模式命中
├── libxml2/2.15.3.1 制品仓 | CML 格式,cmake_find_mode=none + LIBXML2_* 注入(D11)
├── rapidjson/1.1.0 制品仓 | JSON 格式,RAPIDJSON_* 确定性短路避免外拉(D11)
├── zlib OHOS SDK sysroot 共享 libz.so(D10)
├── inchi 源码内置 src/formats/libinchi(WITH_INCHI=ON)
└── boost 不引入(上游非 REQUIRED)
CMake 关键选项(conanfile.py generate() 注入,全部 cache 变量):CMAKE_POLICY_VERSION_MINIMUM=3.5(D12);BUILD_SHARED=ON;CMAKE_SHARED_LIBRARY_SONAME_CXX_FLAG/C_FLAG=-Wl,-soname,(D3,坑 3);CMAKE_LIBRARY_OUTPUT_DIRECTORY=<build>/lib(D4);LIB_INSTALL_DIR=<package>/lib 绝对路径(D5,坑 5);LIBXML2_INCLUDE_DIR/LIBXML2_LIBRARY、RAPIDJSON_FOUND/INCLUDE_DIRS/VERSION(D11);ZLIB_INCLUDE_DIR/ZLIB_LIBRARY*(D10)。WITH_INCHI=ON、ENABLE_TESTS=ON 保持上游默认。
坑 1(D1):共享构建下所有格式转换失败
-
现象:
BUILD_SHARED=ON后上游 ctest 大量失败:SetInFormat()恒返回 false、FindType()恒返回 null,格式转换全部报"未知格式"。 -
定位(多轮排查):先怀疑插件 .so 没生成——检查构建树,127 个插件齐全;再怀疑加载路径——手动设置
BABEL_LIBDIR依旧失败。转折点是符号比对:nm -D对比发现OBFormat::Map()在 libopenbabel.so 里是强定义,在每个插件 .so 里却各有一份 WEAK 私有副本。读 plugin.h 真相浮出:默认分支把Map()定义为头内 inline 的 function-local static——多 DSO 模型下每个 .so 各持一份注册表,插件注册进 A 表、调用方查 B 表,Map().count(ID)恒 0。最意外的发现:上游早已为 Cygwin/MinGW 的同一问题备好正解(Map()外置声明 +PLUGIN_CPP_FILE库内单点定义),只是用#if defined(__CYGWIN__)||defined(__MINGW32__)圈死了。 -
方案(最小改动):补丁组 1(10 补丁中的 8 个):plugin.h 删除平台条件、无条件采用外置定义路径;7 个 .cpp(chargemodel/descriptor/fingerprint/forcefield/format/op/ops-loader)解除
PLUGIN_CPP_FILE调用的同守卫。补丁 0 处平台宏,可移植形式。// plugin.h(补丁后):无条件外置声明 static BaseClassMap& Map(); // 7 个 .cpp(解除守卫后):库内单点强定义 PLUGIN_CPP_FILE(OBFormat) -
经验:头文件里的"静态单例/注册表"在多 DSO 模型下先问一句"它有几份";上游若已为其他平台备好正解,优先复用那条路径而不是新打补丁。
-
入档:本案已入档知识条目 E1216(错误签名 out-of-line-registry-singleton,含对照组结论),条目见第五节。
坑 2(D2):修完注册表,test_runner 仍 SIGSEGV
-
现象:注册表修复后 ctest 主流程打通,但一批用例直接崩溃:
dynamic_cast<OBPairFloatingPoint*>处 SIGSEGV。 -
定位(多轮排查):崩溃点在 dynamic_cast 失败返回 null 之后的解引用——问题不在指针,在 cast 本身判失败。查 Itanium ABI:
type_info::operator==只比较指针地址(唯一 typeinfo 语义);nm -D证实OBPairTemplate<int/double/bool>在 libopenbabel.so、插件、测试可执行文件里各自实例化出 WEAK 的 typeinfo/vtable——指针不同,跨 DSO 恒判失败。同样的代码在上游 Linux 环境不暴露(上游 CI 全绿),两种工具链的具体链接行为差异本文不展开,但 extern template 是 Itanium ABI 下跨 DSO 模板去重的标准解(C++ 标准明确推荐用它控制跨 DSO 实例化)。 -
方案(最小改动):补丁组 2(2 个):generic.h 对三个特化加
extern template声明、generic.cpp 库内单点显式实例化。// generic.h(补丁后):告知所有翻译单元"别自行实例化,库里有" extern template class OBPairTemplate<int>; extern template class OBPairTemplate<double>; extern template class OBPairTemplate<bool>; // generic.cpp(补丁后):库内单点显式实例化 → 各一个强 typeinfo/vtable template class OBPairTemplate<int>; template class OBPairTemplate<double>; template class OBPairTemplate<bool>; -
经验:模板类型跨 DSO 使用时,"头文件 extern template 声明 + 库内单点实例化"是标准解法;看到"多个 DSO 各有 WEAK typeinfo"的模式先想到唯一性问题。
-
入档:本案已入档知识条目 E1202(错误签名 extern-template-single-instantiation),条目见第五节。
坑 3(D3):插件 dlopen 失败,DT_NEEDED 是相对路径
-
现象:注册表修好后主库能查到插件了,但 dlopen 失败——
readelf -d显示插件的 DT_NEEDED 记的是../../bin/libopenbabel.so.8(相对路径),加载器按进程 CWD 相对解析,必然找不到。 -
定位(多轮排查):DT_NEEDED 里出现相对路径本身就不正常——正常链接记的是库名(SONAME)。对比链接命令行发现 OHOS 工具链默认不加
-Wl,-soname:共享库没有 SONAME 时,链接器只能把依赖库的路径写进 DT_NEEDED;而构建树布局又不标准(见坑 4 的 D4),这个路径恰好是../../bin/…。 -
方案(最小改动):
generate()注入 SONAME 旗标(不动源码):tc.cache_variables["CMAKE_SHARED_LIBRARY_SONAME_CXX_FLAG"] = "-Wl,-soname," tc.cache_variables["CMAKE_SHARED_LIBRARY_SONAME_C_FLAG"] = "-Wl,-soname,"CMake 会自动追加库名(完整旗标
-Wl,-soname,libxxx.so传给链接器)。修复后全部 .so 获得规范 SONAME,DT_NEEDED 回归libopenbabel.so.8,经 LD_LIBRARY_PATH 解析。 -
经验:DT_NEEDED 取值异常时先怀疑工具链旗标面,而不是路径配置;工具链默认行为差异优先用 CMake 变量层补齐,不碰源码。
-
入档:本案已入档知识条目 E1174(错误签名 soname-flags-inject),条目见第五节。
坑 4(D4+D6+D7):ctest 找不到库、找不到插件
-
现象:ctest 大面积报找不到库,插件加载全空。
-
定位(多轮排查):起初以为是环境变量没配对,事后补
LD_LIBRARY_PATH无效——打开生成的CTestTestfile.cmake才发现 ENVIRONMENT 行是 configure 期就烘焙定死的($ENV{...}configure 期求值)。两个原因叠加:① OHOS 下if(UNIX)为假,构建树库落<build>/bin,而测试 ENVIRONMENT 烘焙的是<build>/lib(D4);② 插件加载 DLHandler 先读BABEL_LIBDIR,回退的编译期OB_MODULE_PATH(<prefix>/lib/openbabel/3.2.1)在构建阶段还没生成(D7)。 -
方案(最小改动):conanfile.py
build()在 configure 之前把依赖 libdirs + 构建树 lib/bin 预置进os.environ["LD_LIBRARY_PATH"];ctest 之前置BABEL_LIBDIR=<build>/lib(经 ctest 父进程环境继承给各测试):ld = [os.path.join(self.build_folder, "lib"), os.path.join(self.build_folder, "bin")] for dep in self.dependencies.host.values(): for d in dep.cpp_info.aggregated_components().libdirs: p = d if os.path.isabs(d) else os.path.join(dep.package_folder, d) if os.path.isdir(p) and p not in ld: ld.append(p) os.environ["LD_LIBRARY_PATH"] = ":".join(ld) # 必须在 cmake.configure() 之前os.environ["BABEL_LIBDIR"] = os.path.join(self.build_folder, "lib") # ctest 之前 -
经验:CMake
$ENV{...}取值在 configure 期定死——测试依赖的环境变量必须预置而非事后补,这是 Conan 流程里最容易误判的时序问题之一。
坑 5(D5):主库没进包——CMake 4.1.2 安装目录相对值陷阱
-
现象:conan create 跑完,包里没有 libopenbabel.so,test_package 报
Library 'openbabel' not found in package。 -
定位(多轮排查):先怀疑补丁改错/安装规则缺失——两轮日志排查确认
cmake --install里主库零安装行且无任何报错。转折点是最小复现实验:用一个 20 行 CMakeLists 复刻上游对 LIB_INSTALL_DIR 的处理,跑"2 个变量名 × 相对/绝对"共 4 组组合,隔离出触发条件——只有与上游set(CACHE PATH)同名、且-D传入相对值(UNINITIALIZED 缓存条目)的变量,会在类型升级时被解析到CMAKE_BINARY_DIR:安装目的地变成<build>/lib,源=目的,cmake --install静默跳过。绝对值直通(已实测)。 -
方案(最小改动):传包目录绝对路径:
tc.cache_variables["LIB_INSTALL_DIR"] = os.path.join(self.package_folder, "lib")附带修复 .pc 文件装错位置(其 DESTINATION 派生自 LIB_INSTALL_DIR)。
-
经验:CMake 4.x + 老 CMakeLists,凡是"-D 传入的目录变量安装目的地不对",先怀疑 CACHE 类型升级,用最小复现验证触发条件(是否同名、是否相对值),别在 install 规则上盲目排查。
-
入档:本案已入档知识条目 E1179(错误签名 absolute-install-dir),条目见第五节。
坑 6(D9):ctest 不在 conan 进程 PATH
-
现象:
build()内subprocess.run(["ctest", ...])报FileNotFoundError: 'ctest'。 -
定位:conan 进程的 PATH 不含 profile 的 buildenv PATH(buildenv 仅注入
self.run的 shell 环境)——Conan 2 机制差异,一轮排查确认。 -
方案(最小改动):显式解析 + 缺失硬失败:
ctest = shutil.which("ctest") or ( "/data/service/hnp/bin/ctest" if os.path.isfile("/data/service/hnp/bin/ctest") else None) if ctest is None: raise ConanException("ctest 不可用:运行上游测试套件(R51)的硬前提") -
经验:配方里别假设工具在 PATH;对硬前提工具,宁可显式失败也不静默跳过(本配方以上游测试实跑为交付前提)。
坑 7:W3 非确定性失败——环境变量跨阶段泄漏(误判)
-
现象:前面全修好后本地 conan create 全绿,但 W3 验证时 test_package 时好时坏:库有时找到、有时找不到。
-
误判:起初以为是环境变量配置遗漏——反复调整
LD_LIBRARY_PATH/BABEL_LIBDIR组合、甚至怀疑 conan 缓存不一致,失败依旧"偶发"。纠正:读 Conan 2 执行模型才发现 test_package 的 test() 与 build() 同进程执行、继承 os.environ——build() 里设置的BABEL_LIBDIR=<构建树>泄漏进测试进程,覆盖了编译期 OB_MODULE_PATH(包目录)的优先查找(插件查找逻辑:BABEL_LIBDIR 优先,回退 OB_MODULE_PATH,再回退 “./”);构建树目录在测试时是否还存在不确定,于是非确定性。 -
方案(最小改动):build() 在 ctest 结束后存原值/还原;test() 用 shell 前缀直接注入(
ConanFile.run()的 env 参数是 env 脚本文件名列表,传"VAR=VALUE"会被当文件名找不到后静默忽略——同一轮里一并踩到):# build():ctest 结束后恢复环境 for _k, _v in saved_env.items(): if _v is None: os.environ.pop(_k, None) else: os.environ[_k] = _v# test():收集依赖图全部 libdir,shell 前缀注入 ob_pkg = self.dependencies["openbabel"].package_folder babel_libdir = os.path.join(ob_pkg, "lib", "openbabel", "3.2.1") exe = os.path.join(self.build_folder, "test_package_exe") self.run( 'LD_LIBRARY_PATH="{}" BABEL_LIBDIR="{}" "{}"'.format(ld_path, babel_libdir, exe)) -
经验:Conan 2 中 build()/test() 同进程——build() 里改 os.environ 就是"进程级全局变量",必须存/还;
ConanFile.run(env=...)不是 VAR=VALUE 列表,别踩。
坑 8(D8):未签名 ELF 不可执行
-
现象:包生成后,真机上 obabel 无法执行、插件 dlopen 被拒(OHOS 签名校验)。
-
定位:OHOS 拒绝执行/dlopen 未签名 ELF——平台硬约束,非配置问题,一轮确认。
-
方案(最小改动):
package()遍历 bin/ 与 lib/,对全部 ELF 做binary-sign-tool sign -selfSign 1就地签名(soname 符号链接链跳过,指向已签名文件):for dirpath, _dirs, files in os.walk(base): for fn in sorted(files): p = os.path.join(dirpath, fn) if os.path.islink(p): continue # soname 符号链接链指向已签名文件 with open(p, "rb") as f: if f.read(4) != b"\x7fELF": continue self._sign_elf(p) # sign -selfSign 1 就地签名 + chmod 755 -
经验:OHOS 上"全 ELF 签名"是打包步骤的一部分而非事后补救;跳过符号链接(避免对同一文件签两次)。
四、验证(分层,禁止相加)
口径声明:以下各层分别统计、分别陈述,不做相加/改名/拼接——上游 348 项是"上游测试被执行",消费者 4 项是"消费者链路断言",149 是"签名产物数",三者口径不同。
4.1 上游测试实跑(R51 三步法台账)
| 步骤 | 内容 | 结果 |
|---|---|---|
| Step 1 清点 | test/(单数)目录;test/CMakeLists.txt 双重 foreach(cpptests×parts)动态注册 + origtests/inchi/fuzz 回归块 | ctest -N 实测总用例 = 348;测试数据 test/ 36,814 文件 + data/ 65 文件 |
| Step 2 实跑 | 配方 build() 内 ctest --output-on-failure --timeout 120 -j 4 --test-dir <build>;本机 OHOS aarch64 原生执行(与 CI 同构:clang 15 / ohos-sdk_26.0.0.18);前置 = 坑 3 SONAME 修复 + 坑 4 环境预置 | 全量 348 项执行,无跳过 |
| Step 3 回填 | 实测 348/348 通过,通过率 100% | 失败模式分类:0 失败(--output-on-failure 全量输出留档) |
关键日志行(W3 留档日志逐字):
openbabel/3.2.1: 运行上游 ctest 套件(348 项,--timeout 120,-j 4)
...
100% tests passed, 0 tests failed out of 348
Total Test time (real) = 99.81 sec
- 两次独立全跑:首次成功构建 85.13 秒 / W3 干净重建 99.81 秒(均 348/348)——非一次性侥幸。
- 证据已入仓可复现:W3 日志 sha256 =
480a19a36f3ee2447c5ecf5364cba70374032e135e47f5932af0ecc321f983f2,与.ci-result.json的log_sha256一致(本轮已用sha256sum复核);ctest 在conan create内自动执行,复跑见 4.5。
4.2 消费者测试(L2 级:真实 API + 结果断言)
分级声明:本节为 L2 级验证——实例化被测包核心 API、走真实插件加载链路、对输出做结构正确性硬断言(非 print-only,非版本/帮助断言)。覆盖链路:OBConversion 实例化 → 格式插件 dlopen + 注册表查找(smi/inchi)→ 分子解析 → InChI 生成(经 libinchi)→ 输出断言。
测试源码(test_package/test.cpp,完整可复现):
// openbabel 消费者测试:SMILES→InChI 转换硬断言
// 覆盖被测包核心链路:OBConversion 实例化 → 格式插件 dlopen + 注册表查找
// (smi/inchi) → 分子解析 → InChI 生成(经 libinchi)→ 输出断言
#include <openbabel/obconversion.h>
#include <openbabel/mol.h>
#include <iostream>
#include <string>
using namespace OpenBabel;
static int failures = 0;
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
std::cerr << "FAIL: " << msg << std::endl; \
++failures; \
} else { \
std::cout << "ok: " << msg << std::endl; \
} \
} while (0)
int main()
{
OBConversion conv;
OBMol mol;
CHECK(conv.SetInAndOutFormats("smi", "inchi"),
"SetInAndOutFormats(smi, inchi)(插件加载 + 注册表查找)");
if (failures) {
return 1;
}
CHECK(conv.ReadString(&mol, "CCO"), "ReadString(CCO)(SMILES 解析)");
std::string result = conv.WriteString(&mol, true);
CHECK(!result.empty(), "WriteString 产出非空(InChI 生成,经 libinchi)");
CHECK(result.find("InChI=1S/C2H6O") != std::string::npos,
"乙醇 InChI 含 InChI=1S/C2H6O");
if (!result.empty()) {
std::cout << "InChI: " << result << std::endl;
}
if (failures) {
std::cerr << "test_package: " << failures << " 项断言失败" << std::endl;
return 1;
}
std::cout << "test_package: 全部断言通过" << std::endl;
return 0;
}
执行命令与实测输出(2026-10-18 本机实测,逐字):
cd /storage/Users/currentUser/ohpc-contest/openbabel-3.2.1/build_in_harmonyos
conan test archives/o/openbabel/3.2.1/test_package openbabel/3.2.1 -pr:h=ohos-aarch64 -pr:b=ohos-aarch64
ok: SetInAndOutFormats(smi, inchi)(插件加载 + 注册表查找)
ok: ReadString(CCO)(SMILES 解析)
ok: WriteString 产出非空(InChI 生成,经 libinchi)
ok: 乙醇 InChI 含 InChI=1S/C2H6O
InChI: InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3
test_package: 全部断言通过
4 项断言全部通过;输出 InChI(InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3)是乙醇的标准 InChI 字符串——结构正确性由断言而非肉眼保证。
口径边界:该测试断言的是 SMILES→InChI 这一条最具代表性的核心链路(同时穿越插件 dlopen、注册表查找、libinchi 生成三个关键环节);其余 125+ 种格式的转换正确性由上游 348 项套件保障(见 4.1),未在本机逐一重跑。

图1:鸿蒙 PC 真机执行消费者测试,4 项硬断言全部通过(证明消费者链路可用;不证明上游 348 全量)。
4.3 产物核验
| 维度 | 数值 | 来源 |
|---|---|---|
| CMake 目标 | 159(Built target) | W3 conan 构建日志实测 |
| 编译单元 | 403(CXX 356 + C 47) | 同上 |
| 签名 ELF | 149 = 127 插件 + 20 工具 + 2 顶层库 | W3 日志签名 RUN 行去重计数 |
| 包布局 | lib/libopenbabel.so{,.8,.8.0.0} + lib/libinchi.so{,.1,.1.07.5} + lib/openbabel/3.2.1/(127 插件,无 lib 前缀,如 APIInterface.so)+ bin/(20 个命令行工具含 obabel)+ include/openbabel3/openbabel/(版本化头文件目录)+ share/openbabel/3.2.1/(力场参数数据) | W3 日志 + 包目录实测 |
| 头文件 | openbabel 3 头文件装于版本化目录 include/openbabel3/openbabel/;配方 package_info() 设 includedirs=["include/openbabel3"],消费者 #include <openbabel/base.h> 经此解析 | conanfile.py |
| 运行时 | runenv_info 自动导出 BABEL_LIBDIR=<pkg>/lib/openbabel/3.2.1;未设时回退编译期 OB_MODULE_PATH(已指向包内,实测可用) | conanfile.py + 实测 |
| W3 门禁 | L1a(test_package 已执行)/ L1b(测试证据)/ L1c(build 阶段实跑上游 ctest)/ L1e(破坏性负向验证:test 实质依赖 openbabel)/ L2(smoke)/ L4(补丁危险函数扫描)/ L5(产物基线)全部通过;exit_code=0 | .ci-result.json + W3 日志 |
4.4 CI 与评审
完整轮次表(11 个结果轮):
| 轮次 | 构建# | 结果时间 | Worker | 结果 | 说明 |
|---|---|---|---|---|---|
| 1 | #67978 | 10-02 23:52 | CI-023 | ❌ | 作废(基础设施故障):worker-prep/git-clone 阶段 Connection refused:111,Worker 网络受限,配方代码未执行 |
| 2 | #68881 | CI-034 | ❌ | 作废(同上,源码下载超时) | |
| 3 | #70080 | CI-002 | ❌ | 作废(同上) | |
| 4 | #70320 | CI-023 | ❌ | 作废(同上,排队约 4 小时后失败) | |
| 5 | #75488 | CI-014 | ❌ | 作废(同上) | |
| 6 | #75748 | CI-014 | ❌ | 作废(同上) | |
| 7 | #76175 | CI-014 | ❌ | 作废(同上) | |
| 8 | #80508 | 10-10 19:22 | CI-040 | ❌ | 作废(同上) |
| 9 | #85167 | 10-12 01:37 | CI-022 | ✅ | Worker 网络恢复:Conan test_package + L0/L1 门禁全过 |
| 10 | #85351 | 10-12 03:20 | CI-021 | ✅ | 同上 |
| 11 | #86039 | 10-12 12:29 | CI-036 | ✅ | 同上(head = 5162a0ad3) |
- 8 次失败全部同一形态:类别=网络问题 / 问题=源码下载超时 / 原因=Worker 网络受限(机器人诊断原文:已知可达 zlib.net;已知不可达 ftp.gnu.org),全部失败于源码获取阶段——0 次配方/构建阶段失败;CI 是评论触发型,每轮失败后按修复循环
/rebuild重触发(触发跨度 10-02 → 10-11)。 - 检视记录:平台 AI 检视(atomgit-bot)5 次均"✅ 未发现问题"(含新 push 后);本地独立检视 P0=0 / P1=0(P2×1 shebang 缺失已修复,P3×1 shared 选项记录在案,均不阻塞)。
- 合并记录:PR #14369 于 2026-10-13 22:13 合入 main(合入提交
77807f58),关联 Issue #3888(高难度挑战候选确认申请)。 - 发布:PR 合入 main 后由 release 流水线发布到 ohpcd 制品仓——recipe revision
efe11bdc6fc08ae73e08616a5762e592(2026-10-13 14:26 UTC),与本地 W3 export 的 revision 完全一致(交叉验证:本地构建的就是发布的那份配方)。
制品仓发布核验命令与实测输出(2026-10-18 本机实测,逐字):
conan download "openbabel/3.2.1" -r=ohpcd --only-recipe
openbabel
openbabel/3.2.1
revisions
efe11bdc6fc08ae73e08616a5762e592 (2026-10-13 14:26:21 UTC)

图2:制品仓 ohpcd 中 openbabel/3.2.1 的 recipe revision 核验。

图3:PR #14369 已合并页面。
4.5 复现速查
三条命令,全部来自工作区真实日志/留档文件(照抄即可执行):
① 全量构建 + 上游 348 项 + 消费者测试(本机实测 ≈5 分钟,含 ctest;来源:notes.md 编译步骤 + W3 日志):
cd /storage/Users/currentUser/ohpc-contest/openbabel-3.2.1/build_in_harmonyos
conan create archives/o/openbabel/3.2.1 -pr:h=ohos-aarch64 -pr:b=ohos-aarch64 --build=missing
期望输出(关键行,逐字来自 W3 日志):
100% tests passed, 0 tests failed out of 348
...
test_package: 全部断言通过
② 消费者链路快速复跑(秒级)(来源:2026-10-18 本机实测):
conan test archives/o/openbabel/3.2.1/test_package openbabel/3.2.1 -pr:h=ohos-aarch64 -pr:b=ohos-aarch64
期望输出:4 行 ok: … + InChI: InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3 + test_package: 全部断言通过(完整输出见 4.2)。
③ 留档证据链完整性核验(来源:.ci-result.json + 2026-10-18 本机实测):
sha256sum archives/o/openbabel/3.2.1/reports/w3-local-ci.log
期望输出(须与 .ci-result.json 的 log_sha256 一致):
480a19a36f3ee2447c5ecf5364cba70374032e135e47f5932af0ecc321f983f2 archives/o/openbabel/3.2.1/reports/w3-local-ci.log
口径对应:命令① → 4.1(348/348)+ 4.2(4/4)+ 4.3(159 目标/149 签名);命令② → 4.2(4/4);命令③ → 4.1 证据链(日志未被改动)。至此,本文所有核心数字都可以用这三条命令在本机重新长出。
五、知识沉淀
4 条知识条目随 PR 以草稿形式入仓,已由主编审核 promote 进正式知识库(草稿编号 E545–E548,promote 时与他条目去重重编号,正式编号如下,与正式库无冲突):
| 正式编号 | 知识(错误签名) | 来源坑位 | 复用价值 |
|---|---|---|---|
| E1174 | OHOS 工具链默认不设 SONAME 旗标:无 SONAME 时链接器把依赖库相对路径写进消费者 DT_NEEDED,dlopen 按 CWD 相对解析失败;注入 CMAKE_SHARED_LIBRARY_SONAME_{C,CXX}_FLAG=-Wl,-soname,(soname-flags-inject) | 坑 3 | 所有带多层 .so 依赖的 OHOS 共享库配方 |
| E1179 | CMake 4.x:-D 传入的 UNINITIALIZED 相对值安装目录变量,在上游 set(CACHE PATH) 类型升级时解析到 CMAKE_BINARY_DIR → 安装目的地=构建树、cmake --install 静默跳过;此类变量一律传绝对路径(absolute-install-dir) | 坑 5 | 所有老 CMakeLists + 新 CMake 组合的配方 |
| E1202 | 跨 DSO WEAK typeinfo:同名模板在多个 DSO 各自实例化,typeinfo 各自私有,dynamic_cast(Itanium 唯一 typeinfo 语义,operator== 只比指针)恒失败;标准解 = 头文件 extern template 声明 + 库内单点显式实例化(extern-template-single-instantiation) | 坑 2 | 所有多 DSO C++ 模板库 |
| E1216 | 头内 inline function-local static 注册表:多 DSO 模型下每个 DSO 私有持有一份,注册与查找落进不同表(FindType 恒 null);正解 = 删除平台条件、无条件走外置定义路径(库内 PLUGIN_CPP_FILE 单点定义)(out-of-line-registry-singleton) | 坑 1 | 所有"头文件静态单例/注册表"的多 DSO 插件体系 |
状态说明:4 条均为正式条目(knowledge/graph/ + 配套 knowledge/doc/),随 PR #14369 入仓、2026-10-16/17 主编知识库归档批次 promote;编号已去重,与正式库现有条目无冲突。
六、FAQ
Q1:为什么坚持共享构建?静态库不是依赖链更干净吗?
静态构建下上游根本不编译格式插件(仅共享模式构建 MODULE 插件),348 项 ctest 和完整格式转换能力都以共享为前提——静态形态不是"更干净",是"残缺"。依赖链由 Conan 管理,消费者 conan install 自动拉取全部 3 个依赖,链不长。
Q2:10 个补丁是不是太多了?为什么不能一个 #ifdef __OHOS__ 解决?
平台宏只是遮丑:注册表各 DSO 各持一份副本的问题本身没被解决(宏只换实现,不换"多份"的事实);而且这 10 个补丁里 8 个走的是上游为 Cygwin/MinGW 已备好的正解路径,2 个是 C++ 标准推荐的 extern template 惯用法——可移植形式(0 平台宏)在任何多 DSO 平台都成立,具备上游化潜力,比平台特判更可能被社区接受。
Q3:348/348 通过,上游测试体系会不会太弱?
348 项覆盖格式往返、描述符、力场、InChI 校验、fuzz 回归(含 ClusterFuzz 最小化用例),由 36,814 个测试数据文件支撑——对一个 2010 年代起步的化学工具库,这是完整体系而非烟测。但通过的含义要写准:本平台行为与上游期望一致,不构成对化学格式语义正确性的再认证(见诚实声明 1)。
Q4:消费者测试只有 4 项断言,能证明库"能用"吗?
它是 L2 级(真实 API + 结果断言),断言的是最具代表性的核心链路:一次调用同时穿越插件 dlopen、注册表查找、SMILES 解析、InChI 生成(经 libinchi)四个关键环节,任一环节失效都会返回 false 或产出错误字符串。"全库每个格式都验证过"不成立——那是 4.1 的上游 348 项在保障,边界写在 4.2 口径声明里。
Q5:CI 失败 8 次,是不是配方不成熟?
8 次全部失败在 Worker 网络的源码获取阶段(Connection refused:111,配方代码一行未执行),构建阶段失败为 0;网络恢复后连续 3 绿,且期间配方零改动(3 个绿轮 head 相同)。失败轮已逐轮留档并标注作废原因(4.4 表)。
Q6:制品仓不是有 zlib 吗,为什么不走 conan 依赖?
制品仓 zlib/1.3.1 是静态非 -fPIC 构建,链入共享库触发 aarch64 重定位错误(本机实测);OHOS SDK sysroot 自带共享 libz.so 是平台原生设施,配方自动探测最新 SDK 并注入 ZLIB_INCLUDE_DIR/ZLIB_LIBRARY,对上游而言只是"找到了一个 ZLIB",零源码改动。
七、总结
7.1 量化盘点
| 维度 | 数字 |
|---|---|
| 源码补丁 | 10 个(组 1 插件注册表 8 + 组 2 跨 DSO typeinfo 2,0 平台宏) |
| 上游差异点 | 12 个(D1–D12,差异表) |
| 适配问题(笔记台账) | 7 项 + 签名 1 项 = 8 坑 |
| 构建规模 | 159 个 CMake 目标 / 403 编译单元 |
| 签名产物 | 149 = 127 插件 + 20 工具 + 2 顶层库 |
| 上游测试 | 348/348(99.81s,-j4;另一次独立全跑 85.13s) |
| 消费者测试 | 4/4(L2 级硬断言) |
| 测试数据 | 36,814 = 36,168 + 419 + 227(test/),另有 data/ 65 |
| 知识沉淀 | 4 条正式条目(E1174/E1179/E1202/E1216) |
| CI | 11 轮 = 8 网络故障作废 + 3 绿(head 5162a0ad3) |
| 交付 | PR #14369 已合入(10-13);ohpcd 制品仓已发布(revision efe11bdc…) |
7.2 可复用方法(5 条)
- 注册表单实例化:头内 function-local static 注册表 → 外置声明 + 库内单点定义(先查上游是否已为其他平台备好正解)。
- 模板跨 DSO 收敛:
extern template声明 + 库内显式实例化,收敛 WEAK typeinfo。 - 工具链旗标面补齐:先怀疑工具链默认值(SONAME 等),用 CMake 变量层补齐,不碰源码。
- configure 期环境预置:被
$ENV{...}烘焙的环境变量必须在 configure 之前置好,事后补无效。 - 安装目录绝对路径:
-D传入的安装目录变量一律绝对路径(CMake 4.xset(CACHE PATH)类型升级陷阱),配最小复现验证触发条件。
7.3 流程 checklist(通用)
- W0 依赖发布检查(
conan download --only-recipe逐个,缺失先适配发布依赖) - R51 测试三步法:清点(
ctest -N)→ 实跑 → 回填 - 共享/静态选型决策:静态形态是否覆盖测试体系?
- 工具链旗标面检查(SONAME / visibility / rpath)
- CMake 版本兼容(
cmake_minimum_required+ policy minimum) - 依赖注入方式选型(find 模式 CONFIG vs module;静态 vs PIC)
- build() 改 os.environ 必须存/还(Conan 2 同进程继承)
- 全 ELF 签名(OHOS 拒绝执行/dlopen 未签名 ELF)
- W3 本地 CI(日志 sha256 留档,.ci-result.json 禁止手改)
- W4 独立审计(P0/P1 清零)
- CI 完整轮次台账(基础设施故障标作废、注明原因,不删轮)
- 知识沉淀(草稿随 PR 入仓,主编 promote)
7.4 后续工作
- 10 个补丁为可移植形式,可向上游提 PR(上游 Cygwin/MinGW 分支已有同构正解,本修复是自然扩展)。
- 消费者测试可扩展 L3 负例(非法 SMILES、未知格式应报错而非崩溃)。
- 静态构建形态可另行评估:若上游开放插件静态编译,可补第二个形态供嵌入式场景。
参考链接
- PR #14369:https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos/merge_requests/14369
- Issue #3888:https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos/issues/3888
- 配方仓库:https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos
- 上游源:https://github.com/openbabel/openbabel/archive/refs/tags/openbabel-3-2-1.tar.gz
- 官网:https://openbabel.org
- CI 日志:https://ohpcd.atomgit.com/ci?id=86039
更多推荐




所有评论(0)