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++ 工具库,提供:
- 字符串处理(
Ztring、ZtringList等) - 文件操作(
File、Dir、FileName) - 线程和同步(
Thread、CriticalSection) - 配置解析(
InfoMap、Translation) - 大整数支持(
int128s/int128u)
它是MediaInfo和MediaConch的核心依赖。在 OpenHarmony 上移植 MediaInfo 前,必须先完成 ZenLib 的交叉编译。
1.2 为什么单独做成独立包
上游 MediaInfoLib 的 CMakeLists.txt 硬编码了一个相对路径../../../ZenLib/Source来引用 ZenLib 头文件,这种方式无法用于交叉编译。正确的做法是:
- 将ZenLib 编译安装为一个独立的系统包,提供 CMake Config 文件 (
ZenLibConfig.cmake) - 在 MediaInfoLib 中通过
find_package(ZenLib)查找依赖,BUILD_ZENLIB=OFF禁止内嵌构建 - 安装路径由
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>&1CMake 的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不同:
| 维度 | HPKBUILD | HPKCHECK |
|---|---|---|
| 执行时机 | 交叉编译时(在构建机上运行) | 安装到真机后(在 OHOS 设备上运行) |
| 执行环境 | Linux x86_64 + OHOS toolchain | OpenHarmony 设备(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}三个关键约定:
source HPKBUILD— 导入 HPKBUILD 中定义的元变量(pkgname、pkgdir、builddir、ARCH等),确保测试脚本与构建脚本使用同一套路径约定logfile— 测试日志输出路径,框架会自动收集并归档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,建议按以下顺序检查:
是否有现成的测试套件?
- 若项目通过 CTest / make check 注册了测试 → 用模式 B
- 若无 → 考虑模式 C(至少做产检)或模式 A(占位)
构建目录路径是否正确?
- CMake 项目:
${builddir}/Project/CMake/$ARCH-build(如 libzen、libmediainfo) - 也有项目用
${builddir}/$ARCH-build(如 xnnpack、libpng) - 以 HPKBUILD 中
build()函数的cmake -B路径为准
- CMake 项目:
失败时是否保留足够现场?
- 至少复制测试日志到
LYCIUM_FAULT_PATH - 对于二进制产物检查,可以复制
.so文件供离线分析
- 至少复制测试日志到
测试日志是否可读?
- 关键步骤使用
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文件,改用sed在prepare()中直接做文本替换:
sed-i's|#if !defined(__ANDROID_API__)|#if !defined(__ANDROID_API__) && !defined(__MUSL__)|'\Source/ZenLib/Thread.cpps|旧|新|语法用|作为分隔符,避免与路径中的/冲突。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.pc4.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-v7a | 486 KB | 1.1 MB |
| arm64-v8a | 555 KB | 1.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.patch | OHOS 适配补丁(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.patch | CMakeLists.txt 移除硬编码 ZenLib 路径(CRLF,仅供参考) |