HarmonyOS鸿蒙PC三方库移植:使用 lycium框架移植libzen (ZenLib)踩坑笔记

📅 2026/7/9 2:44:15 👁️ 阅读次数 📝 编程学习
HarmonyOS鸿蒙PC三方库移植:使用 lycium框架移植libzen (ZenLib)踩坑笔记

本文记录在使用 lycium编译框架移植工作完成后自动生成,完整的技术细节和踩坑经验。

相关资源地址:

开源鸿蒙通用三方库社区地址:https://gitcode.com/oh-tpc
tpc_c_cplusplus框架介绍地址:https://gitcode.com/openharmony-sig/tpc_c_cplusplus

lycium_plusplus框架地址:https://gitcode.com/OpenHarmonyPCDeveloper/lycium_plusplus
OpenHarmony PC Developer 社区:https://gitcode.com/OpenHarmonyPCDeveloper

目标平台: OpenHarmony (OHOS) — armeabi-v7a / arm64-v8a
上游版本: ZenLib v0.4.41
编译框架: lycium / HPKBUILD
最终用途: 作为 MediaInfoLib (libmediainfo) 的依赖


移植成功截图:

1. 背景

1.1 ZenLib 是什么

ZenLib 是 MediaArea 项目维护的一套跨平台 C++ 工具库,提供:

  • 字符串处理(ZtringZtringList等)
  • 文件操作(FileDirFileName
  • 线程和同步(ThreadCriticalSection
  • 配置解析(InfoMapTranslation
  • 大整数支持(int128s/int128u

它是MediaInfoMediaConch的核心依赖。在 OpenHarmony 上移植 MediaInfo 前,必须先完成 ZenLib 的交叉编译。

1.2 为什么单独做成独立包

上游 MediaInfoLib 的 CMakeLists.txt 硬编码了一个相对路径../../../ZenLib/Source来引用 ZenLib 头文件,这种方式无法用于交叉编译。正确的做法是:

  1. ZenLib 编译安装为一个独立的系统包,提供 CMake Config 文件 (ZenLibConfig.cmake)
  2. 在 MediaInfoLib 中通过find_package(ZenLib)查找依赖,BUILD_ZENLIB=OFF禁止内嵌构建
  3. 安装路径由CMAKE_INSTALL_PREFIX控制,输出到统一仓库lycium/usr/

2. 移植步骤

2.1 目录结构

thirdparty/libzen/ ├── HPKBUILD # 构建脚本 ├── HPKCHECK # 测试占位 ├── libzen_ohos.patch # OHOS 适配补丁 └── download.log # (自动生成) 下载日志

2.2 HPKBUILD 核心说明

点击展开 HPKBUILD 完整内容
pkgname=libzenpkgver=0.4.41pkgrel=0pkgdesc="ZenLib - C++ utility library used by MediaArea projects (MediaInfo, MediaConch)."url="https://github.com/MediaArea/ZenLib"archs=("armeabi-v7a""arm64-v8a")license=("BSD-2-Clause")depends=()makedepends=()source="https://github.com/MediaArea/ZenLib/archive/refs/tags/v$pkgver.tar.gz"autounpack=truedownloadpackage=truebuildtools="cmake"# GitHub tarball tag "v0.4.41" extracts to "ZenLib-0.4.41/"builddir=ZenLib-${pkgver}packagename=v$pkgver.tar.gzpatchflag=trueprepare(){# 打patch: OHOS(musl) 不支持 pthread_cancelif$patchflagthencd$builddir# 源码文件是 CRLF 换行,用 sed 直接替换规避 patch 换行符不兼容sed-i's|#if !defined(__ANDROID_API__)|#if !defined(__ANDROID_API__) \&\& !defined(__MUSL__)|'Source/ZenLib/Thread.cpppatchflag=falsecd$OLDPWDfimkdir-p$builddir/Project/CMake/$ARCH-build}build(){cd$builddirPKG_CONFIG_LIBDIR="${pkgconfigpath}"${OHOS_SDK}/native/build-tools/cmake/bin/cmake"$@"\-DOHOS_ARCH=$ARCH-BProject/CMake/$ARCH-build -SProject/CMake/-L>$buildlog2>&1$MAKEVERBOSE=1-CProject/CMake/$ARCH-build>>$buildlog2>&1ret=$?cd${OLDPWD}return$ret}package(){cd$builddir$MAKEVERBOSE=1-CProject/CMake/$ARCH-buildinstall>>$buildlog2>&1ret=$?cd${OLDPWD}return$ret}check(){if[$ARCH=="armeabi-v7a"]thencp-af$OHOS_SDK/native/llvm/lib/arm-linux-ohos/libc++_shared.so$LYCIUM_ROOT/usr/$pkgname/$ARCH/lib/elif[$ARCH=="arm64-v8a"]thencp-af$OHOS_SDK/native/llvm/lib/aarch64-linux-ohos/libc++_shared.so$LYCIUM_ROOT/usr/$pkgname/$ARCH/lib/elseecho"$ARCHnot support"return-1fiecho"The test must be on an OpenHarmony device!"}# 清理环境cleanbuild(){rm-rf${PWD}/$builddir}

关键变量说明:

变量含义
builddir=ZenLib-${pkgver}GitHub tagv0.4.41解压后目录名为ZenLib-0.4.41
packagename=v$pkgver.tar.gz下载的文件名为v0.4.41.tar.gz
buildtools="cmake"使用 CMake 构建(而非 autotools)
depends=()ZenLib 不依赖任何第三方库(纯 C++,仅依赖 libc++)

HPKCHECK :

sourceHPKBUILD>/dev/null2>&1logfile=${LYCIUM_THIRDPARTY_ROOT}/${pkgname}/${pkgname}_${ARCH}_${OHOS_SDK_VER}_test.logopenharmonycheck(){res=0cd$builddir/Project/CMake/$ARCH-build# ZenLib has no ctest-based tests to run on deviceecho"The test must be on an OpenHarmony device!">${logfile}2>&1cd$OLDPWDreturn$res}

2.3 构建管道

prepare() → 解压 → 打 patch → 创建构建目录 build() → cmake 配置 → make 编译 package() → make install → 复制产物到 lycium/usr/libzen/$ARCH/ check() → 安装 libc++_shared.so → 提示真机测试
prepare() 阶段

在 CMake 配置前,对Source/ZenLib/Thread.cpp做一行替换:

sed-i's|#if !defined(__ANDROID_API__)|#if !defined(__ANDROID_API__) && !defined(__MUSL__)|'Source/ZenLib/Thread.cpp

原因详见第 3 节(坑点记录)。

build() 阶段
cd$builddirPKG_CONFIG_LIBDIR="${pkgconfigpath}"${OHOS_SDK}/native/build-tools/cmake/bin/cmake\"$@"-DOHOS_ARCH=$ARCH\-BProject/CMake/$ARCH-build -SProject/CMake/-L>$buildlog2>&1$MAKEVERBOSE=1-CProject/CMake/$ARCH-build>>$buildlog2>&1

使用 OpenHarmony SDK 内置的 cmake toolchain 进行交叉编译:

  • cmake 入口:Project/CMake/(非源码根目录)
  • 构建目录:Project/CMake/$ARCH-build
  • 选项-L:列出所有 CMake cache 变量到日志,方便排查
package() 阶段
$MAKEVERBOSE=1-CProject/CMake/$ARCH-buildinstall>>$buildlog2>&1

CMake 的install目标会自动将库、头文件、cmake-config 文件安装到CMAKE_INSTALL_PREFIX指定的路径(由build.sh传入)。

check() 阶段

复制libc++_shared.so到安装目录,确保在真机上运行时能找到 C++ 运行时库:

cp-af$OHOS_SDK/native/llvm/lib/aarch64-linux-ohos/libc++_shared.so\$LYCIUM_ROOT/usr/$pkgname/$ARCH/lib/

不同架构的 libc++_shared.so 路径不同:arm-linux-ohos/对应 armeabi-v7a,aarch64-linux-ohos/对应 arm64-v8a。


2.4 HPKCHECK:测试脚本

2.4.1 HPKCHECK 是什么

HPKCHECK是 lycium 框架中与HPKBUILD配套的测试描述文件,位于同一个库目录下(如thirdparty/libzen/HPKCHECK)。它定义了在OpenHarmony 真机上对已编译库进行功能测试的流程。

HPKBUILD不同:

维度HPKBUILDHPKCHECK
执行时机交叉编译时(在构建机上运行)安装到真机后(在 OHOS 设备上运行)
执行环境Linux x86_64 + OHOS toolchainOpenHarmony 设备(arm)
产出.so/.a/ 头文件测试日志*_test.log
核心函数prepare()build()package()openharmonycheck()
必备性必须推荐

2.4.2 HPKCHECK 的基本结构

# 1. source HPKBUILD — 获取 pkgname、builddir 等变量sourceHPKBUILD>/dev/null2>&1# 2. 定义测试日志路径logfile=${LYCIUM_THIRDPARTY_ROOT}/${pkgname}/${pkgname}_${ARCH}_${OHOS_SDK_VER}_test.log# 3. (可选) 测试前的准备工作checkprepare(){return0}# 4. 核心测试函数 — 在真机上被调用openharmonycheck(){res=0cd${builddir}/Project/CMake/${ARCH}-build# ... 测试逻辑 ...cd$OLDPWDreturn$res}

三个关键约定:

  1. source HPKBUILD— 导入 HPKBUILD 中定义的元变量(pkgnamepkgdirbuilddirARCH等),确保测试脚本与构建脚本使用同一套路径约定
  2. logfile— 测试日志输出路径,框架会自动收集并归档
  3. openharmonycheck()— 唯一的入口函数,返回 0 表示测试通过,非 0 表示失败

2.4.3 三种测试模式

从现有库的 HPKCHECK 可以归纳出三种模式:

模式 A:占位/人工测试(适用于无自动化测试的库)

适用场景:库本身不提供测试套件,或测试需要在真实环境中手动执行。

libzen为例:

sourceHPKBUILD>/dev/null2>&1logfile=${LYCIUM_THIRDPARTY_ROOT}/${pkgname}/${pkgname}_${ARCH}_${OHOS_SDK_VER}_test.logopenharmonycheck(){res=0cd$builddir/Project/CMake/$ARCH-buildecho"The test must be on an OpenHarmony device!">${logfile}2>&1cd$OLDPWDreturn$res}

特点:

  • 不执行任何自动化测试
  • 仅在日志中提示测试需要在真机上手动执行
  • 始终返回 0(不阻塞 CI 流程)
模式 B:ctest / make test(适用于 CMake 项目)

适用场景:项目通过 CTest 或 make check 注册了单元测试。

xnnpack(CMake + CTest)为例:

sourceHPKBUILD>/dev/null2>&1logfile=${LYCIUM_THIRDPARTY_ROOT}/${pkgname}/${pkgname}_${ARCH}_${OHOS_SDK_VER}_test.logopenharmonycheck(){res=0cd${builddir}/${ARCH}-build ctest>${logfile}2>&1res=$?if[$res-ne0];thenmkdir-p${LYCIUM_FAULT_PATH}/${pkgname}cpTesting/Temporary/LastTest.log${LYCIUM_FAULT_PATH}/${pkgname}/ficd$OLDPWDreturn$res}

核心模式:

步骤含义
cd ${builddir}/...-build进入构建目录(CTest 配置文件在此)
ctest > ${logfile}执行全部 CTest 测试用例
mkdir -p ${LYCIUM_FAULT_PATH}+cp LastTest.log测试失败时保留现场日志
return $res将测试结果传给框架

如果需要过滤特定用例(如排除需要 Python 的测试),可以使用ctest -I

# googletest 的写法:只跑第 1~45 个用例ctest-I1,45>${logfile}2>&1
模式 C:自定义产检检查(适用于无测试套件的库)

适用场景:库没有注册 CTest 用例,但仍希望在真机上验证基本功能。

libmediainfo为例(本文联合编写的 HPKCHECK):

sourceHPKBUILD>/dev/null2>&1logfile=${LYCIUM_THIRDPARTY_ROOT}/${pkgname}/${pkgname}_${ARCH}_${OHOS_SDK_VER}_test.logopenharmonycheck(){res=0cd${builddir}/Project/CMake/${ARCH}-build# 1. 验证共享库已生成if[!-flibmediainfo.so];thenecho"FAIL: libmediainfo.so not found">>${logfile}res=1fi# 2. 验证核心导出符号if[$res-eq0];thenifnm-Dlibmediainfo.so|grep-q"MediaInfo_New";thenecho"OK: MediaInfo_New symbol found">>${logfile}elseecho"WARN: symbol not found">>${logfile}fifiif[$res-ne0];thenmkdir-p${LYCIUM_FAULT_PATH}/${pkgname}cplibmediainfo.so${LYCIUM_FAULT_PATH}/${pkgname}/ficd$OLDPWDreturn$res}

模式 C 的适用场景:

  • 库体量大、完整的真机测试成本高(需要特定媒体文件)
  • 但希望至少验证产物的基本完整性(文件存在、符号导出正确)
  • 作为 “smoke test” 防线,在 CI 中快速反馈链接是否正确

2.4.4 编写 HPKCHECK 的 checklist

编写一个高质量的 HPKCHECK,建议按以下顺序检查:

  1. 是否有现成的测试套件?

    • 若项目通过 CTest / make check 注册了测试 → 用模式 B
    • 若无 → 考虑模式 C(至少做产检)或模式 A(占位)
  2. 构建目录路径是否正确?

    • CMake 项目:${builddir}/Project/CMake/$ARCH-build(如 libzen、libmediainfo)
    • 也有项目用${builddir}/$ARCH-build(如 xnnpack、libpng)
    • 以 HPKBUILD 中build()函数的cmake -B路径为准
  3. 失败时是否保留足够现场?

    • 至少复制测试日志到LYCIUM_FAULT_PATH
    • 对于二进制产物检查,可以复制.so文件供离线分析
  4. 测试日志是否可读?

    • 关键步骤使用echo "OK/FAIL: ..." >> ${logfile}输出
    • 避免裸命令静默执行而无任何日志输出

3. 踩坑记录

坑 1:pthread_cancel在 musl 中缺失

现象
Source/ZenLib/Thread.cpp:522:5: error: use of undeclared identifier 'pthread_cancel' pthread_cancel((pthread_t)ThreadPointer); ^ 1 error generated.
根因
  • pthread_cancel是 glibc / Android Bionic 提供的非 POSIX 标准函数
  • OpenHarmony 使用 musl libc,musl 未实现pthread_cancel(作者认为该设计有缺陷)
  • ZenLib 此前已对 Android 做了规避(!defined(__ANDROID_API__)),但未考虑 musl
解决

修改Source/ZenLib/Thread.cpp第 522 行,在条件中增加!defined(__MUSL__)

// Before:#if!defined(__ANDROID_API__)pthread_cancel((pthread_t)ThreadPointer);#endif// After:#if!defined(__ANDROID_API__)&&!defined(__MUSL__)pthread_cancel((pthread_t)ThreadPointer);#endif

为什么用__MUSL__
OHOS cmake toolchain 的编译器定义中自动包含了-D__MUSL__(可从编译日志的 CFLAGS 中看到)。

远程影响评估

ForceTerminate()不再会被调用时,线程将无法被强制终止。但考虑该函数的设计意图仅是"非优雅终止",在 OHOS 上此功能降级为"等待线程自然退出",不影响正常功能。

坑 2:GitHub 源码文件的 CRLF 换行符

现象
patching file Source/ZenLib/Thread.cpp Hunk #1 FAILED at 519 (different line endings).

patch工具报错"不同的行结束符"。

根因

ZenLib 的源码文件使用CRLF(Windows 风格,\r\n)行结束符,而patch工具默认处理的 patch 文件使用LF(Unix 风格,\n),两者不兼容。

解决

放弃使用patch文件,改用sedprepare()中直接做文本替换:

sed-i's|#if !defined(__ANDROID_API__)|#if !defined(__ANDROID_API__) && !defined(__MUSL__)|'\Source/ZenLib/Thread.cpp

s|旧|新|语法用|作为分隔符,避免与路径中的/冲突。sed -i原地修改,保留原始文件的换行符格式。

注意:libzen_ohos.patch文件保留在仓库中以记录改动意图,但实际构建时通过sed执行。

坑 3:GitHub 直连下载失败

现象
HTTP request sent, awaiting response... No data received.

wget请求 GitHub release tarball 时服务器无响应。

根因

该环境的网络对github.com的某些端点不稳定,连接被 reset。

解决

改用 GitHub 的codeload端点:

# 原始地址(可能超时)https://github.com/MediaArea/ZenLib/archive/refs/tags/v0.4.41.tar.gz# 替代地址(codeload,通常更稳定)https://codeload.github.com/MediaArea/ZenLib/tar.gz/v0.4.41

如果 codeload 也不通,还可以使用 jsDelivr CDN 镜像:

https://cdn.jsdelivr.net/gh/MediaArea/ZenLib@v0.4.41

但 jsDelivr 对于大文件可能需要.zip格式而非.tar.gz,且不递归解压子模块。


4. 编译结果验证

4.1 构建产物

lycium/usr/libzen/ ├── armeabi-v7a/ # 32-bit ARM 架构 │ ├── include/ZenLib/ # 头文件 (30 个 .h) │ ├── lib/ │ │ ├── libc++_shared.so # C++ 运行时 (来自 OHOS SDK) │ │ ├── libzen.so → libzen.so.0 │ │ ├── libzen.so.0 → libzen.so.0.4.41 │ │ ├── libzen.so.0.4.41 # 共享库 (486 KB) │ │ ├── cmake/zenlib/ # CMake config (供 find_package) │ │ └── pkgconfig/libzen.pc # pkg-config 描述文件 │ └── ... (同上结构) └── arm64-v8a/ # 64-bit ARM 架构 ├── include/ZenLib/ # 头文件 (与 32-bit 相同) ├── lib/ │ ├── libc++_shared.so │ ├── libzen.so → libzen.so.0 │ ├── libzen.so.0 → libzen.so.0.4.41 │ └── libzen.so.0.4.41 # 共享库 (555 KB) ├── cmake/zenlib/ └── pkgconfig/libzen.pc

4.2 自动注册到构建记录

编译成功后,build.sh会将库注册到lycium/usr/hpk_build.csv

libzen,0.4.41,armeabi-v7a libzen,0.4.41,arm64-v8a

这意味着后续编译依赖 libzen 的库(如 libmediainfo)时,框架会自动跳过 libzen 的重复编译。

4.3 文件大小对比

架构libzen.so 大小libc++_shared.so
armeabi-v7a486 KB1.1 MB
arm64-v8a555 KB1.2 MB

32-bit 库体积略小,属于正常的指令集差异。

4.4 CMake 集成验证

ZenLibConfig.cmake正确导出了zen目标:

# 依赖方 CMakeLists.txt 中使用方式: find_package(ZenLib REQUIRED) target_link_libraries(myapp PRIVATE zen) # → 自动添加头文件路径和链接选项

5. 与 libmediainfo 的联动

libzen 之所以被移植,根本目的是作为libmediainfo (MediaInfoLib)的依赖。两者在构建时的关系:

zlib (交叉编译自 Zlib 源码) └── libzen (本篇文章的目标库) └── libmediainfo (最终目标)

编译顺序:

./build.sh zlib # 先编译 zlib(基本依赖) ./build.sh libzen # 再编译 ZenLib ✅ 已完成 ./build.sh libmediainfo # 最后编译 MediaInfoLib(依赖 libzen + zlib)

libmediainfo 的 HPKBUILD 中声明了:

depends=("libzen""zlib")

构建时通过-DBUILD_ZENLIB=OFF -DBUILD_ZLIB=OFF禁止内嵌编译,改为find_package查找系统已安装的版本。CMake 的CMAKE_FIND_ROOT_PATH机制自动将lycium/usr/libzen/$ARCH/lycium/usr/zlib/$ARCH/纳入搜索路径。


6. 完整命令速查

# 环境准备exportOHOS_SDK=/root/ohos-sdk/linux# 单次编译(从下载到安装)cd/root/build/tpc_c_cplusplus/lycium ./build.sh libzen# 清理并重新编译rm-rf../thirdparty/libzen/ZenLib-0.4.41../thirdparty/libzen/*.log ./build.sh libzen# 手动验证产物fileusr/libzen/arm64-v8a/lib/libzen.so.0.4.41 nm-Dusr/libzen/arm64-v8a/lib/libzen.so.0.4.41|grep-i"zenlib"# 查看编译日志(排查错误时使用)tail-100../thirdparty/libzen/libzen-0.4.41-arm64-v8a-lycium_build.log

附:文件清单

libzen

文件用途
thirdparty/libzen/HPKBUILD构建脚本 (87 行,含 pthread_cancel sed 替换)
thirdparty/libzen/HPKCHECK测试占位(提示真机手动测试)
thirdparty/libzen/libzen_ohos.patchOHOS 适配补丁(pthread_cancel修复,CRLF,仅供参考)
thirdparty/libzen/download.log下载日志(自动生成)
thirdparty/libzen/libzen-*.log编译日志(自动生成,每个架构一个)
lycium/usr/libzen/安装产物(头文件 + .so + cmake config)

libmediainfo

文件用途
thirdparty/libmediainfo/HPKBUILD构建脚本(依赖 libzen + zlib,BUILD_ZENLIB=OFF)
thirdparty/libmediainfo/HPKCHECK测试脚本(产检模式:验证 .so 存在 + MediaInfo_New 符号)
thirdparty/libmediainfo/libmediainfo_ohos.patchCMakeLists.txt 移除硬编码 ZenLib 路径(CRLF,仅供参考)