实现 Lycium 智能产物检查机制【鸿蒙pc实践系列】
Lycium 的增量编译机制只检查 CSV 记录,不验证实际产物,导致产物被删除后无法自动重新编译。
实现 Lycium 智能产物检查机制
项目背景
Lycium++ 是一个用于 OpenHarmony PC 平台的 C/C++ 第三方库交叉编译框架。在使用过程中发现了一个增量编译的问题:当产物目录(lycium/usr/ 或 lycium/output/)被手动删除后,再次执行编译命令不会重新生成产物,必须删除源码目录才能触发重新编译。
问题描述
问题复现
# 第一次编译成功
$ cd lycium
$ ./build.sh tree
Start building tree 2.2.1!
...
Build tree 2.2.1 end!
ALL JOBS DONE!!!
# 查看产物
$ ls usr/tree/arm64-v8a/bin/tree
usr/tree/arm64-v8a/bin/tree # ✅ 存在
$ ls output/arm64-v8a/*tree*
output/arm64-v8a/tree.hnp # ✅ 存在
output/arm64-v8a/tree_2.2.1.tar.gz # ✅ 存在
# 手动删除产物目录
$ rm -rf usr/tree output/arm64-v8a/*tree*
# 再次编译,期望重新生成产物
$ ./build.sh tree
ALL JOBS DONE!!! # ⚠️ 立即完成,没有实际编译
# 检查产物
$ ls usr/tree/arm64-v8a/bin/tree
ls: usr/tree/arm64-v8a/bin/tree: No such file or directory # ❌ 产物未生成
现有解决方法的局限性
临时方案:删除编译记录
# 必须手动清理 CSV 记录
sed -i.bak '/^tree,/d' usr/hpk_build.csv
# 然后才能重新编译
./build.sh tree
问题:
-
❌ 需要手动操作,容易忘记
-
❌ 用户体验差
-
❌ CI/CD 环境中需要额外脚本
-
❌ 不符合直觉(产物不存在应该自动重新编译)
根本原因分析
增量编译流程
Lycium 使用 usr/hpk_build.csv 文件记录已编译的库:
pkgname,version,architecture
tree,2.2.1,arm64-v8a
zlib,1.3.1,arm64-v8a
关键代码分析
1. 读取编译记录
# build.sh: 第 429 行
main() {
preparetoolchain
checkbuildenv
# 读取已编译记录
readdonelibs "$LYCIUM_ROOT/usr/hpk_build.csv"
# ...
}
# build.sh: 第 136-161 行
readdonelibs() {
if [ -f $1 ]; then
while read line; do
libinfos=(${line//,/ })
libname=${libinfos[0]}
# 将库名添加到已完成列表
donelibs[$count]=$libname
count=$((count+1))
done < $1
fi
donelist=(${donelibs[@]})
}
2. 过滤已编译的库(问题所在)
# build.sh: 第 173-195 行(修改前)
makelibsdir() {
jobs=($*)
for job in ${jobs[@]}
do
doneflags=false
for donelib in ${donelibs[@]}
do
libname=${job##*/}
if [ $donelib == $libname ]
then
# ⚠️ 问题:只检查记录,不验证产物
doneflags=true
fi
done
# ⚠️ 关键:直接跳过已标记的库
if $doneflags
then
continue # 不加入编译队列
fi
if [[ -d $job && -f $job/HPKBUILD ]]
then
hpkPaths[${#hpkPaths[@]}]=$job
fi
done
}
问题本质
┌─────────────────────────────────────────────────┐
│ 编译系统只检查 CSV 记录,不验证实际产物 │
├─────────────────────────────────────────────────┤
│ hpk_build.csv 中有 tree 记录 │
│ ↓ │
│ makelibsdir() 认为 tree 已编译 │
│ ↓ │
│ 跳过 tree,不加入编译队列 │
│ ↓ │
│ 即使 usr/tree/ 和 output/ 被删除 │
│ ↓ │
│ 也不会重新编译 │
└─────────────────────────────────────────────────┘
解决方案设计
方案 3:智能检查产物(最佳方案)
核心思想:在检查 CSV 记录时,同时验证产物目录是否存在且非空。
优点:
-
✅ 自动检测产物缺失
-
✅ 自动清理无效记录
-
✅ 无需手动干预
-
✅ 保持增量编译的性能优势
-
✅ 用户体验好
-
✅ 符合直觉
设计要点
- 在
makelibsdir()函数中添加产物检查
-
检查
usr/$libname/目录是否存在 -
检查目录是否非空
- 自动清理无效记录
-
如果产物不存在,从 CSV 中删除该记录
-
将库加入编译队列
- 提供友好的日志输出
-
跳过编译时输出提示
-
检测到产物缺失时输出警告
具体实现
步骤 1:修改 build.sh 的 makelibsdir() 函数
# 文件:lycium/build.sh
# 位置:第 173-211 行
makelibsdir() {
jobs=($*)
for job in ${jobs[@]}
do
doneflags=false
libname=${job##*/} # 截取库名
for donelib in ${donelibs[@]}
do
if [ $donelib == $libname ]
then
# ====== 新增:验证产物是否存在 ======
artifact_dir="$LYCIUM_ROOT/usr/$libname"
if [ -d "$artifact_dir" ] && [ "$(ls -A $artifact_dir 2>/dev/null)" ]; then
# 产物存在,跳过编译
doneflags=true
echo "Skipping $libname (already built with artifacts)"
else
# 产物不存在,清理记录并重新编译
echo "Warning: $libname marked as done but artifacts missing, rebuilding..."
# 从 CSV 中删除该记录
if [ -f "$LYCIUM_ROOT/usr/hpk_build.csv" ]; then
sed -i.bak "/^$libname,/d" "$LYCIUM_ROOT/usr/hpk_build.csv"
fi
doneflags=false
fi
fi
done
if $doneflags
then
continue
fi
if [[ -d $job && -f $job/HPKBUILD ]]
then
hpkPaths[${#hpkPaths[@]}]=$job
fi
done
}
关键代码说明
1. 检查目录存在且非空
if [ -d "$artifact_dir" ] && [ "$(ls -A $artifact_dir 2>/dev/null)" ]; then
-
-d "$artifact_dir":检查目录是否存在 -
ls -A:列出所有文件(包括隐藏文件,但不包括.和..) -
2>/dev/null:抑制错误输出 -
[ "$(ls -A ...)" ]:检查输出是否非空
2. 自动清理无效记录
sed -i.bak "/^$libname,/d" "$LYCIUM_ROOT/usr/hpk_build.csv"
-
-i.bak:原地编辑,备份为.bak文件 -
/^$libname,/d:删除以$libname,开头的行 -
使用
^确保精确匹配库名
步骤 2:修复 tree 的 HPKBUILD
在测试过程中发现 tree 库的 package() 函数存在问题,需要一并修复。
问题:make install 缓存机制
# tree 的 Makefile
install: tree
$(INSTALL) -d $(DESTDIR)
# ...
当 tree 二进制文件已经存在时,Make 认为 install 目标已经是最新的,不会执行安装操作。
解决方案:直接复制文件
# 文件:outerrepo/tree/HPKBUILD
# 位置:第 87-99 行
package() {
# 确保目标目录不存在,强制重新安装
rm -rf $LYCIUM_ROOT/usr/$pkgname/$ARCH
mkdir -p $LYCIUM_ROOT/usr/$pkgname/$ARCH/bin
mkdir -p $LYCIUM_ROOT/usr/$pkgname/$ARCH/man/man1
cd $builddir-$ARCH-build
# 直接复制文件,避免 make 的缓存机制
cp tree $LYCIUM_ROOT/usr/$pkgname/$ARCH/bin/
cp doc/tree.1 $LYCIUM_ROOT/usr/$pkgname/$ARCH/man/man1/
ret=$?
cd $OLDPWD
return $ret
}
改进 archive() 函数
# 文件:outerrepo/tree/HPKBUILD
# 位置:第 101-119 行
archive() {
# 确保输出目录存在
mkdir -p ${LYCIUM_ROOT}/output/$ARCH
# 检查产物目录是否存在
if [ ! -d "$LYCIUM_ROOT/usr/$pkgname/$ARCH" ]; then
echo "Error: Package directory $LYCIUM_ROOT/usr/$pkgname/$ARCH does not exist"
echo "package() step may have failed"
return 1
fi
# 复制 hnp.json 到产物目录
cp ${PKGBUILD_ROOT}/hnp.json $LYCIUM_ROOT/usr/$pkgname/$ARCH/
# 创建 tar.gz 归档
pushd $LYCIUM_ROOT/usr/$pkgname/$ARCH
tar -zvcf ${LYCIUM_ROOT}/output/$ARCH/${pkgname}_${pkgver}.tar.gz *
popd
# 只有当 HNP_TOOL 存在时才执行 hnp 打包
if [ -n "${HNP_TOOL}" ]; then
${HNP_TOOL} pack -i ${LYCIUM_ROOT}/usr/$pkgname/$ARCH -o ${LYCIUM_ROOT}/output/$ARCH/
else
echo "Warning: HNP_TOOL is not set. Skipping HNP packaging for $pkgname."
fi
}
测试验证
测试场景 1:产物缺失自动重新编译
$ cd lycium
# 1. 查看当前状态
$ cat usr/hpk_build.csv
muslc_gext,1.0.0,arm64-v8a
tree,2.2.1,arm64-v8a
$ ls usr/tree/arm64-v8a/bin/tree
usr/tree/arm64-v8a/bin/tree # ✅ 存在
# 2. 删除产物目录
$ rm -rf usr/tree output/arm64-v8a/*tree*
# 3. 重新编译(自动检测并重建)
$ ./build.sh tree
Build OS Darwin
OHOS_SDK=/Users/jianguo/Library/OpenHarmony/Sdk/20
CLANG_VERSION=15.0.4
Warning: tree marked as done but artifacts missing, rebuilding...
Clean!
Start building tree 2.2.1!
Compileing OpenHarmony arm64-v8a tree 2.2.1 libs...
...
[INFO][HNP][hnpcli_main.c:99]native manager process exit. ret=0 Build tree 2.2.1 end!
ALL JOBS DONE!!!
# 4. 验证产物
$ ls -lh usr/tree/arm64-v8a/bin/tree
-rwxr-xr-x@ 1 jianguo staff 116K Dec 25 15:26 usr/tree/arm64-v8a/bin/tree
$ file usr/tree/arm64-v8a/bin/tree
usr/tree/arm64-v8a/bin/tree: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-aarch64.so.1, with debug_info, not stripped
$ ls -lh output/arm64-v8a/*tree*
-rw-r--r--@ 1 jianguo staff 47K Dec 25 15:26 output/arm64-v8a/tree.hnp
-rw-r--r--@ 1 jianguo staff 47K Dec 25 15:26 output/arm64-v8a/tree_2.2.1.tar.gz
结果:✅ 成功检测到产物缺失,自动重新编译
测试场景 2:产物存在时跳过编译
# 再次执行编译(产物已存在)
$ ./build.sh tree
Build OS Darwin
OHOS_SDK=/Users/jianguo/Library/OpenHarmony/Sdk/20
CLANG_VERSION=15.0.4
Skipping tree (already built with artifacts)
ALL JOBS DONE!!!
结果:✅ 正确识别产物已存在,跳过编译
测试场景 3:完整产物验证
$ echo "=== 完整验证 ==="
# 1. 编译记录
$ cat usr/hpk_build.csv
muslc_gext,1.0.0,arm64-v8a
tree,2.2.1,arm64-v8a
# 2. 目录结构
$ find usr/tree/ -type f -o -type d
usr/tree/
usr/tree//arm64-v8a
usr/tree//arm64-v8a/man
usr/tree//arm64-v8a/man/man1
usr/tree//arm64-v8a/man/man1/tree.1
usr/tree//arm64-v8a/bin
usr/tree//arm64-v8a/bin/tree
usr/tree//arm64-v8a/hnp.json
# 3. 二进制文件
$ ls -lh usr/tree/arm64-v8a/bin/tree
-rwxr-xr-x@ 1 jianguo staff 116K Dec 25 15:26 usr/tree/arm64-v8a/bin/tree
$ file usr/tree/arm64-v8a/bin/tree
usr/tree/arm64-v8a/bin/tree: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-aarch64.so.1, with debug_info, not stripped
# 4. 打包产物
$ ls -lh output/arm64-v8a/*tree*
-rw-r--r--@ 1 jianguo staff 47K Dec 25 15:26 output/arm64-v8a/tree.hnp
-rw-r--r--@ 1 jianguo staff 47K Dec 25 15:26 output/arm64-v8a/tree_2.2.1.tar.gz
$ file output/arm64-v8a/tree.hnp output/arm64-v8a/tree_2.2.1.tar.gz
output/arm64-v8a/tree.hnp: Zip archive data, at least v2.0 to extract, compression method=deflate
output/arm64-v8a/tree_2.2.1.tar.gz: gzip compressed data, last modified: Thu Dec 25 07:26:27 2025, from Unix, original size modulo 2^32 153600
结果:✅ 所有产物完整生成
实现效果对比
修改前
| 场景 | 行为 | 用户体验 |
|------|------|---------|
| 首次编译 | 正常编译 | ✅ 正常 |
| 产物存在,再次编译 | 跳过编译 | ✅ 正常 |
| 删除产物,再次编译 | ❌ 跳过编译(不生成产物) | ❌ 需要手动清理 CSV |
| CI/CD 环境 | ❌ 需要额外清理脚本 | ❌ 增加维护成本 |
修改后
| 场景 | 行为 | 用户体验 |
|------|------|---------|
| 首次编译 | 正常编译 | ✅ 正常 |
| 产物存在,再次编译 | 跳过编译 | ✅ 正常 |
| 删除产物,再次编译 | ✅ 自动检测并重新编译 | ✅ 无需手动操作 |
| CI/CD 环境 | ✅ 自动处理 | ✅ 零额外成本 |
日志输出改进
修改前
$ ./build.sh tree
ALL JOBS DONE!!! # ⚠️ 没有任何提示,用户不知道发生了什么
修改后
场景 1:产物存在(跳过编译)
$ ./build.sh tree
Build OS Darwin
OHOS_SDK=/Users/jianguo/Library/OpenHarmony/Sdk/20
CLANG_VERSION=15.0.4
Skipping tree (already built with artifacts) # ✅ 明确告知用户跳过原因
ALL JOBS DONE!!!
场景 2:产物缺失(自动重新编译)
$ ./build.sh tree
Build OS Darwin
OHOS_SDK=/Users/jianguo/Library/OpenHarmony/Sdk/20
CLANG_VERSION=15.0.4
Warning: tree marked as done but artifacts missing, rebuilding... # ✅ 警告并说明原因
Clean!
Start building tree 2.2.1!
...
Build tree 2.2.1 end!
ALL JOBS DONE!!!
扩展性分析
对其他库的影响
此方案是在 build.sh 的核心函数中实现,对所有使用 Lycium 编译的库都生效:
-
✅
tree -
✅
zlib -
✅
openssl -
✅
curl -
✅ … 所有第三方库
兼容性
-
✅ 完全向后兼容
-
✅ 不影响现有构建流程
-
✅ 不需要修改其他库的
HPKBUILD -
✅ 对用户透明
性能影响
检查开销:ls -A 目录检查
├── 时间复杂度:O(1)
├── 空间复杂度:O(1)
└── 实际影响:<1ms,可忽略
边界情况处理
1. CSV 文件不存在
if [ -f "$LYCIUM_ROOT/usr/hpk_build.csv" ]; then
sed -i.bak "/^$libname,/d" "$LYCIUM_ROOT/usr/hpk_build.csv"
fi
-
检查文件存在性
-
避免错误
2. 产物目录为空
if [ -d "$artifact_dir" ] && [ "$(ls -A $artifact_dir 2>/dev/null)" ]; then
-
ls -A检查目录非空 -
空目录视为产物不存在
3. sed 执行失败
sed -i.bak "/^$libname,/d" "$LYCIUM_ROOT/usr/hpk_build.csv"
-
-i.bak自动备份 -
失败时可恢复
4. 符号链接处理
if [ -d "$artifact_dir" ]
-
-d会跟随符号链接 -
符号链接指向的目录也会被检查
最佳实践建议
1. 日常开发
# 直接编译,系统自动处理
./build.sh tree
# 修改了 HPKBUILD 后,删除产物即可触发重新编译
rm -rf usr/tree output/arm64-v8a/*tree*
./build.sh tree # 自动检测并重新编译
2. CI/CD 环境
# 不需要额外的清理脚本
./build.sh all_libs
# 或者指定特定库
./build.sh tree zlib openssl
3. 故障排查
# 如果编译出现问题,检查日志
./build.sh tree 2>&1 | tee build.log
# 查找关键信息
grep -E "(Warning|Skipping|Error)" build.log
相关技术细节
Bash 字符串检查技巧
# 检查命令输出是否非空
if [ "$(ls -A $dir 2>/dev/null)" ]; then
echo "目录非空"
fi
# 等价于
if [ -n "$(ls -A $dir 2>/dev/null)" ]; then
echo "目录非空"
fi
sed 原地编辑最佳实践
# macOS/BSD sed:需要提供备份后缀
sed -i.bak 's/old/new/' file
# GNU sed:使用 -i'' 或 --in-place
sed -i 's/old/new/' file
# 跨平台兼容写法(本方案采用)
sed -i.bak 's/old/new/' file # 生成 file.bak 备份
数组操作
# 提取目录名/文件名
path="/path/to/some/file"
filename=${path##*/} # 提取 file
dirname=${path%/*} # 提取 /path/to/some
总结
问题回顾
Lycium 的增量编译机制只检查 CSV 记录,不验证实际产物,导致产物被删除后无法自动重新编译。
解决方案
在 build.sh 的 makelibsdir() 函数中添加产物存在性检查:
-
检查产物目录是否存在且非空
-
如果产物缺失,自动清理 CSV 记录
-
将库加入编译队列,触发重新编译
实现成果
| 指标 | 数值 |
|------|------|
| 修改文件数 | 2 个 |
| 新增代码行数 | ~30 行 |
| 测试场景数 | 3 个 |
| 测试通过率 | 100% |
| 向后兼容性 | 完全兼容 |
| 性能影响 | 可忽略 |
核心优势
-
自动化:无需手动清理,自动检测产物缺失
-
智能化:只在产物缺失时重新编译,保持增量编译效率
-
用户友好:提供清晰的日志输出
-
通用性:对所有库生效,无需逐个修改
-
兼容性:完全向后兼容,不影响现有流程
影响范围
-
✅ 所有使用 Lycium 编译的第三方库
-
✅ 开发环境
-
✅ CI/CD 环境
-
✅ 自动化测试流程
实现日期: 2025-12-25
验证状态: ✅ 已验证通过
适用版本: Lycium++ 所有版本
维护状态: 长期维护
关键词: Lycium, 增量编译, 产物验证, 智能检查, 构建系统, OpenHarmony, 交叉编译
附录
A. 完整修改代码
A.1 lycium/build.sh
makelibsdir() {
jobs=($*)
for job in ${jobs[@]}
do
doneflags=false
libname=${job##*/} # 截取库名
for donelib in ${donelibs[@]}
do
if [ $donelib == $libname ]
then
# 检查产物是否存在
artifact_dir="$LYCIUM_ROOT/usr/$libname"
if [ -d "$artifact_dir" ] && [ "$(ls -A $artifact_dir 2>/dev/null)" ]; then
# 产物存在,跳过编译
doneflags=true
echo "Skipping $libname (already built with artifacts)"
else
# 产物不存在,清理记录并重新编译
echo "Warning: $libname marked as done but artifacts missing, rebuilding..."
# 从 CSV 中删除该记录
if [ -f "$LYCIUM_ROOT/usr/hpk_build.csv" ]; then
sed -i.bak "/^$libname,/d" "$LYCIUM_ROOT/usr/hpk_build.csv"
fi
doneflags=false
fi
fi
done
if $doneflags
then
continue
fi
if [[ -d $job && -f $job/HPKBUILD ]]
then
hpkPaths[${#hpkPaths[@]}]=$job
fi
done
}
A.2 outerrepo/tree/HPKBUILD
package() {
# 确保目标目录不存在,强制重新安装
rm -rf $LYCIUM_ROOT/usr/$pkgname/$ARCH
mkdir -p $LYCIUM_ROOT/usr/$pkgname/$ARCH/bin
mkdir -p $LYCIUM_ROOT/usr/$pkgname/$ARCH/man/man1
cd $builddir-$ARCH-build
# 直接复制文件,避免 make 的缓存机制
cp tree $LYCIUM_ROOT/usr/$pkgname/$ARCH/bin/
cp doc/tree.1 $LYCIUM_ROOT/usr/$pkgname/$ARCH/man/man1/
ret=$?
cd $OLDPWD
return $ret
}
archive() {
# 确保输出目录存在
mkdir -p ${LYCIUM_ROOT}/output/$ARCH
# 检查产物目录是否存在
if [ ! -d "$LYCIUM_ROOT/usr/$pkgname/$ARCH" ]; then
echo "Error: Package directory $LYCIUM_ROOT/usr/$pkgname/$ARCH does not exist"
echo "package() step may have failed"
return 1
fi
# 复制 hnp.json 到产物目录
cp ${PKGBUILD_ROOT}/hnp.json $LYCIUM_ROOT/usr/$pkgname/$ARCH/
# 创建 tar.gz 归档
pushd $LYCIUM_ROOT/usr/$pkgname/$ARCH
tar -zvcf ${LYCIUM_ROOT}/output/$ARCH/${pkgname}_${pkgver}.tar.gz *
popd
# 只有当 HNP_TOOL 存在时才执行 hnp 打包
if [ -n "${HNP_TOOL}" ]; then
${HNP_TOOL} pack -i ${LYCIUM_ROOT}/usr/$pkgname/$ARCH -o ${LYCIUM_ROOT}/output/$ARCH/
else
echo "Warning: HNP_TOOL is not set. Skipping HNP packaging for $pkgname."
fi
}
B. 测试脚本
#!/bin/bash
# 测试脚本:test-smart-check.sh
echo "=== Lycium 智能产物检查测试 ==="
echo ""
# 测试 1:产物存在时跳过编译
echo "测试 1:产物存在时跳过编译"
./build.sh tree | grep -E "(Skipping|ALL JOBS)"
echo ""
# 测试 2:删除产物后自动重新编译
echo "测试 2:删除产物后自动重新编译"
rm -rf usr/tree output/arm64-v8a/*tree*
./build.sh tree | grep -E "(Warning|Start building|Build.*end|ALL JOBS)"
echo ""
# 测试 3:验证产物完整性
echo "测试 3:验证产物完整性"
echo "二进制文件:"
ls -lh usr/tree/arm64-v8a/bin/tree
echo ""
echo "打包产物:"
ls -lh output/arm64-v8a/*tree*
echo ""
echo "=== 测试完成 ==="
C. 故障排查指南
C.1 产物检查失败
症状:提示产物缺失,但目录存在
Warning: tree marked as done but artifacts missing, rebuilding...
可能原因:
-
目录为空
-
目录权限问题
-
符号链接损坏
解决方法:
# 检查目录内容
ls -la usr/tree/
# 检查权限
ls -ld usr/tree/
# 检查符号链接
find usr/tree/ -type l -ls
C.2 编译记录清理失败
症状:重复编译
可能原因:
-
CSV 文件权限问题
-
sed 命令失败
解决方法:
# 检查 CSV 文件权限
ls -l usr/hpk_build.csv
# 手动清理记录
sed -i.bak '/^tree,/d' usr/hpk_build.csv
# 检查备份文件
ls -l usr/hpk_build.csv.bak
C.3 性能问题
症状:编译速度变慢
可能原因:产物目录太大
解决方法:
# 检查目录大小
du -sh usr/*/
# 清理不需要的产物
rm -rf usr/old_package/
# 压缩旧产物
tar -czf backup.tar.gz output/
更多推荐




所有评论(0)