实现 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 记录时,同时验证产物目录是否存在且非空。

优点

  • ✅ 自动检测产物缺失

  • ✅ 自动清理无效记录

  • ✅ 无需手动干预

  • ✅ 保持增量编译的性能优势

  • ✅ 用户体验好

  • ✅ 符合直觉

设计要点

  1. makelibsdir() 函数中添加产物检查
  • 检查 usr/$libname/ 目录是否存在

  • 检查目录是否非空

  1. 自动清理无效记录
  • 如果产物不存在,从 CSV 中删除该记录

  • 将库加入编译队列

  1. 提供友好的日志输出
  • 跳过编译时输出提示

  • 检测到产物缺失时输出警告

具体实现

步骤 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.shmakelibsdir() 函数中添加产物存在性检查:

  1. 检查产物目录是否存在且非空

  2. 如果产物缺失,自动清理 CSV 记录

  3. 将库加入编译队列,触发重新编译

实现成果

| 指标 | 数值 |

|------|------|

| 修改文件数 | 2 个 |

| 新增代码行数 | ~30 行 |

| 测试场景数 | 3 个 |

| 测试通过率 | 100% |

| 向后兼容性 | 完全兼容 |

| 性能影响 | 可忽略 |

核心优势

  1. 自动化:无需手动清理,自动检测产物缺失

  2. 智能化:只在产物缺失时重新编译,保持增量编译效率

  3. 用户友好:提供清晰的日志输出

  4. 通用性:对所有库生效,无需逐个修改

  5. 兼容性:完全向后兼容,不影响现有流程

影响范围

  • ✅ 所有使用 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...

可能原因

  1. 目录为空

  2. 目录权限问题

  3. 符号链接损坏

解决方法


# 检查目录内容

ls -la usr/tree/



# 检查权限

ls -ld usr/tree/



# 检查符号链接

find usr/tree/ -type l -ls

C.2 编译记录清理失败

症状:重复编译

可能原因

  1. CSV 文件权限问题

  2. 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/

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐