欢迎加入开源鸿蒙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 层面,没有一行配置能替代。

声明:

  1. 348 项上游测试全部在本鸿蒙 PC aarch64 真机原生执行(与 CI 环境同构:clang 15 / ohos-sdk_26.0.0.18);通过 = “本平台行为与上游期望一致”,不构成对化学格式语义正确性的再认证。
  2. 消费者测试为 L2 级(真实 API + 结果断言),覆盖 SMILES→InChI 核心链路;其余 125+ 种格式转换正确性由上游 348 项套件(含逐格式 fuzz 回归)保障,未在本机逐一重跑。

目录

  1. 背景:为什么值得做
  2. 环境与差异前置
  3. 适配过程(8 个坑,四段式)
  4. 验证(4.1 上游实跑 / 4.2 消费者 / 4.3 产物 / 4.4 CI 与评审 / 4.5 复现速查)
  5. 知识沉淀
  6. FAQ
  7. 总结

一、背景:为什么值得做

生态位(第一层):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-W32param get const.product.model
系统HarmonyOS 6.1.0(build 115)uname -s(HarmonyOS);param get const.ohos.fullname(OpenHarmony-6.1.0.115)
架构aarch64uname -m
编译器clang/clang++ 15.0.4/data/service/hnp/bin/clang --version
构建工具CMake 4.1.2 / ctest 4.1.2cmake --version / ctest --version
构建系统Conan 2.29.1(OHOS 适配版)conan --version
OHOS SDKohos-sdk_26.0.0.18ls /data/service/hnp/ohos-sdk.org/
Conan profileos=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-256e140c25480fe1678d00b9a52462368fa4e7805fba67b12ee496784437f3e239econandata.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 恒失败,紧随其后空指针解引用
D3SONAME工具链默认给共享库设 SONAME,DT_NEEDED 记库名OHOS 工具链默认不加 -Wl,-soname;无 SONAME 时链接器把依赖库相对路径写进消费者 DT_NEEDED,dlopen 按进程 CWD 相对解析而失败
D4构建树库位置CMake if(UNIX) 为真,库放 <build>/libCMAKE_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 指向构建树
D8ELF 执行构建出的 .so/可执行文件可直接 dlopen/执行OHOS 内核拒绝执行/dlopen 未签名 ELF → 149 个产物须就地自签名
D9ctest 查找ctest 在 PATH 中conan 进程的 PATH 不含 profile buildenv(仅注入 self.run 的 shell 环境)→ 需显式解析
D10zlibfind_package(ZLIB) 可选,任意 zlib 可链制品仓 conan zlib/1.3.1 为静态非 -fPIC,链入共享库触发 aarch64 重定位错误 → 用 sysroot 共享 libz.so
D11libxml2/rapidjson 探测module 模式 find_package(LibXml2) 判 LIBXML2_FOUND;RAPIDJSON_VERSION 缺失可接受(外拉)CMake 3.24+ PREFER_CONFIG 被 conan 生成的 Config 文件抢占(只置 LibXml2_FOUND);外拉 github 网络不稳 → cmake_find_mode=none + 确定性变量注入
D12CMake 版本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 测试清点):

  1. 依赖发布检查:conan download <pkg>/<ver> -r=ohpcd --only-recipe 逐个探测 eigen/3.4.1、libxml2/2.15.3.1、rapidjson/1.1.0 → 全部命中,可开工(缺失则须先适配发布依赖,再回来构建下游)。
  2. 构建系统侦察:CMake(cmake_minimum_required(2.8.12),2014 年时代的脚本),插件以 MODULE 方式构建、运行时 dlopen 加载。
  3. 测试清点(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)同上
签名 ELF149 = 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#6797810-02 23:52CI-023❌作废(基础设施故障):worker-prep/git-clone 阶段 Connection refused:111,Worker 网络受限,配方代码未执行
2#68881CI-034❌作废(同上,源码下载超时)
3#70080CI-002❌作废(同上)
4#70320CI-023❌作废(同上,排队约 4 小时后失败)
5#75488CI-014❌作废(同上)
6#75748CI-014❌作废(同上)
7#76175CI-014❌作废(同上)
8#8050810-10 19:22CI-040❌作废(同上)
9#8516710-12 01:37CI-022✅Worker 网络恢复:Conan test_package + L0/L1 门禁全过
10#8535110-12 03:20CI-021✅同上
11#8603910-12 12:29CI-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 时与他条目去重重编号,正式编号如下,与正式库无冲突):

正式编号知识(错误签名)来源坑位复用价值
E1174OHOS 工具链默认不设 SONAME 旗标:无 SONAME 时链接器把依赖库相对路径写进消费者 DT_NEEDED,dlopen 按 CWD 相对解析失败;注入 CMAKE_SHARED_LIBRARY_SONAME_{C,CXX}_FLAG=-Wl,-soname,(soname-flags-inject)坑 3所有带多层 .so 依赖的 OHOS 共享库配方
E1179CMake 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)
CI11 轮 = 8 网络故障作废 + 3 绿(head 5162a0ad3)
交付PR #14369 已合入(10-13);ohpcd 制品仓已发布(revision efe11bdc…)

7.2 可复用方法(5 条)

  1. 注册表单实例化:头内 function-local static 注册表 → 外置声明 + 库内单点定义(先查上游是否已为其他平台备好正解)。
  2. 模板跨 DSO 收敛:extern template 声明 + 库内显式实例化,收敛 WEAK typeinfo。
  3. 工具链旗标面补齐:先怀疑工具链默认值(SONAME 等),用 CMake 变量层补齐,不碰源码。
  4. configure 期环境预置:被 $ENV{...} 烘焙的环境变量必须在 configure 之前置好,事后补无效。
  5. 安装目录绝对路径:-D 传入的安装目录变量一律绝对路径(CMake 4.x set(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
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐