解决 Lycium 增量编译中的产物丢失问题

问题描述

在使用 Lycium++ 构建系统时,发现了一个增量编译的问题:

# 第一次编译成功
$ ./build.sh tree
ALL JOBS DONE!!!

# 手动删除产物目录
$ rm -rf lycium/usr/tree lycium/output

# 再次编译,期望重新生成产物
$ ./build.sh tree
ALL JOBS DONE!!!  # ← 立即完成,没有实际编译

# 检查产物
$ ls lycium/usr/tree
ls: lycium/usr/tree: No such file or directory  # ❌ 产物未生成

问题现象

  • ✅ 第一次编译成功,生成所有产物
  • ❌ 删除 lycium/usr/lycium/output/ 目录后
  • ❌ 再次编译会立即完成(ALL JOBS DONE),但不生成产物
  • ❌ 必须删除 outerrepo/tree/ 才能触发重新编译

问题分析

编译系统的增量编译机制

Lycium 使用 lycium/usr/hpk_build.csv 文件记录已编译的库:

pkgname,version,architecture
tree,2.2.1,arm64-v8a
zlib,1.3.1,arm64-v8a
openssl,3.0.0,arm64-v8a

执行流程

1. build.sh 主流程
main() {
    preparetoolchain           # 准备工具链
    checkbuildenv             # 检查环境
    readdonelibs "$LYCIUM_ROOT/usr/hpk_build.csv"  # ← 读取已编译记录
    collectcommunitylibs      # 收集社区库
    findarglibsdir $*         # 查找待编译库
    prepareshell              # 准备脚本
    buildhpk                  # 执行编译
    cleanhpkdir               # 清理
}
2. readdonelibs() 函数
readdonelibs() {
    if [ -f $1 ]; then
        count=0
        while read line; do
            libinfos=(${line//,/ })
            libname=${libinfos[0]}  # 提取库名
            
            # 添加到已完成列表
            donelibs[$count]=$libname
            count=$((count+1))
        done < $1
    fi
    donelist=(${donelibs[@]})
}

读取 CSV 文件,将所有库名存入 donelibs 数组。

3. makelibsdir() 函数
makelibsdir() {
    jobs=($*)
    for job in ${jobs[@]}; do
        doneflags=false
        libname=${job##*/}  # 提取库名
        
        # 检查是否在已编译列表中
        for donelib in ${donelibs[@]}; do
            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,不加入编译队列            │
│  → 即使 usr/tree/ 被删除               │
│  → 也不会重新编译                       │
└─────────────────────────────────────────┘

为什么删除 outerrepo/tree/ 可以触发编译?

findarglibsdir() {
    outerlib=$LYCIUM_ROOT/$outerrepodir/$lib
    if [[ -d $outerlib && -f $outerlib/HPKBUILD ]]; then
        tmplibs[${#tmplibs[@]}]=$outerlib
    else
        # outerrepo/tree 不存在,重新克隆
        python3 load_outer_parts.py --manifestfile=...
    fi
    makelibsdir ${tmplibs[@]}
}

删除 outerrepo/tree/ 后:

  1. Git 会重新克隆仓库
  2. build_hpk.sh 重新链接到新目录
  3. 新的构建过程不受旧 CSV 记录影响(因为重新链接了脚本)

解决方案

方案 1:手动清理编译记录(临时方案)

清理特定库
cd lycium

# 方法 A:使用 sed 删除特定行
sed -i.bak '/^tree,/d' usr/hpk_build.csv

# 方法 B:使用 grep 过滤
grep -v '^tree,' usr/hpk_build.csv > usr/hpk_build.csv.tmp
mv usr/hpk_build.csv.tmp usr/hpk_build.csv

# 重新编译
./build.sh tree
清空所有记录
# 完全清空 CSV(谨慎使用!)
> lycium/usr/hpk_build.csv

# 或删除整个文件
rm lycium/usr/hpk_build.csv

# 重新编译所有库
./build.sh

方案 2:添加强制编译参数(推荐)

修改 build.sh,添加 --force 参数支持。

修改代码
main() {
    # 准备编译工具链
    preparetoolchain
    # 检查编译环境
    checkbuildenv
    
    # ====== 新增:解析命令行参数 ======
    FORCE_BUILD=false
    LIBS=()
    for arg in "$@"; do
        if [ "$arg" == "--force" ] || [ "$arg" == "-f" ]; then
            FORCE_BUILD=true
            echo "Force rebuild mode enabled"
        else
            LIBS+=("$arg")
        fi
    done
    
    # ====== 修改:条件读取编译记录 ======
    if [ "$FORCE_BUILD" == "false" ]; then
        # 读取编译记录
        readdonelibs "$LYCIUM_ROOT/usr/hpk_build.csv"
    else
        echo "Skipping build record check"
    fi
    
    # 搜集 communitydir 目录库
    collectcommunitylibs $communitydir
    
    # ====== 修改:使用 LIBS 数组 ======
    if [ ${#LIBS[@]} -ne 0 ]; then
        # 搜集指定的libs
        findarglibsdir "${LIBS[@]}"
    else
        # 搜集所有可编译的库
        findhpkdir $hpksdir
    fi
    
    # 准备脚本软链接
    prepareshell ${hpkPaths[@]}
    # 编译
    buildhpk
    # 清理
    cleanhpkdir
    unset LYCIUM_BUILD_OS LYCIUM_ROOT CLANG_VERSION
}
使用方式
# 强制重新编译单个库
./build.sh --force tree

# 强制重新编译多个库
./build.sh --force tree zlib openssl

# 强制重新编译所有库
./build.sh --force

# 简写形式
./build.sh -f tree

方案 3:智能检查产物(最佳方案)

修改 makelibsdir() 函数,在检查 CSV 记录时同时验证产物是否存在。

修改代码
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
}
优点
  • ✅ 自动检测产物缺失
  • ✅ 自动清理无效记录
  • ✅ 无需手动干预
  • ✅ 保持增量编译的性能优势
验证测试
# 第一次编译
$ ./build.sh tree
Start building tree 2.2.1!
...
Build tree 2.2.1 end!
ALL JOBS DONE!!!

# 删除产物
$ rm -rf lycium/usr/tree lycium/output

# 第二次编译(自动检测并重新编译)
$ ./build.sh tree
Warning: tree marked as done but artifacts missing, rebuilding...
Start building tree 2.2.1!
...
Build tree 2.2.1 end!
ALL JOBS DONE!!!

方案 4:清理脚本(辅助工具)

创建一个清理脚本简化操作。

创建脚本
#!/bin/bash

# Lycium 清理脚本
# 用于清理编译记录和产物

LYCIUM_ROOT=$(cd $(dirname ${BASH_SOURCE[0]}); pwd)

show_help() {
    cat << EOF
Usage: $0 [OPTIONS] [LIBRARY...]

清理 Lycium 构建产物和记录

OPTIONS:
  -h, --help        显示此帮助信息
  -a, --all         清理所有库的记录和产物
  -r, --record      仅清理编译记录(保留产物)
  -o, --output      仅清理 output 目录
  -u, --usr         仅清理 usr 目录

EXAMPLES:
  $0 tree                # 清理 tree 的记录和产物
  $0 -r tree             # 仅清理 tree 的编译记录
  $0 --all               # 清理所有记录和产物
  $0 tree zlib openssl   # 清理多个库

EOF
}

clean_library() {
    local libname=$1
    local clean_record=${2:-true}
    local clean_usr=${3:-true}
    local clean_output=${4:-true}
    
    echo "Cleaning $libname..."
    
    # 清理编译记录
    if [ "$clean_record" == "true" ]; then
        if [ -f "$LYCIUM_ROOT/usr/hpk_build.csv" ]; then
            sed -i.bak "/^$libname,/d" "$LYCIUM_ROOT/usr/hpk_build.csv"
            echo "  ✓ Removed from build record"
        fi
    fi
    
    # 清理 usr 目录
    if [ "$clean_usr" == "true" ]; then
        if [ -d "$LYCIUM_ROOT/usr/$libname" ]; then
            rm -rf "$LYCIUM_ROOT/usr/$libname"
            echo "  ✓ Removed usr/$libname/"
        fi
    fi
    
    # 清理 output 目录
    if [ "$clean_output" == "true" ]; then
        for arch_dir in "$LYCIUM_ROOT/output"/*; do
            if [ -d "$arch_dir" ]; then
                rm -f "$arch_dir/${libname}"*.tar.gz "$arch_dir/${libname}".hnp
                echo "  ✓ Removed output artifacts"
            fi
        done
    fi
}

# 解析参数
CLEAN_RECORD=true
CLEAN_USR=true
CLEAN_OUTPUT=true
LIBS=()

while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            show_help
            exit 0
            ;;
        -a|--all)
            echo "Cleaning all libraries..."
            rm -f "$LYCIUM_ROOT/usr/hpk_build.csv"
            rm -rf "$LYCIUM_ROOT/usr"/*
            rm -rf "$LYCIUM_ROOT/output"/*
            echo "✓ All cleaned"
            exit 0
            ;;
        -r|--record)
            CLEAN_USR=false
            CLEAN_OUTPUT=false
            shift
            ;;
        -o|--output)
            CLEAN_RECORD=false
            CLEAN_USR=false
            shift
            ;;
        -u|--usr)
            CLEAN_RECORD=false
            CLEAN_OUTPUT=false
            shift
            ;;
        *)
            LIBS+=("$1")
            shift
            ;;
    esac
done

# 清理指定的库
if [ ${#LIBS[@]} -eq 0 ]; then
    echo "Error: No library specified"
    echo "Use --help for usage information"
    exit 1
fi

for lib in "${LIBS[@]}"; do
    clean_library "$lib" "$CLEAN_RECORD" "$CLEAN_USR" "$CLEAN_OUTPUT"
done

echo ""
echo "Cleanup completed. You can now rebuild with:"
echo "  ./build.sh ${LIBS[@]}"
使用示例
# 赋予执行权限
chmod +x lycium/clean.sh

# 清理单个库
./clean.sh tree

# 清理多个库
./clean.sh tree zlib openssl

# 只清理编译记录(保留产物)
./clean.sh -r tree

# 只清理产物(保留记录)
./clean.sh -u tree

# 清理所有
./clean.sh --all

最佳实践

1. 开发调试时

# 修改了 HPKBUILD 后强制重新编译
./build.sh --force tree

# 或先清理再编译
./clean.sh tree && ./build.sh tree

2. CI/CD 环境

# 完全清理后构建(确保干净环境)
./clean.sh --all
./build.sh all_libs

3. 日常使用

# 正常编译(使用增量编译)
./build.sh tree

# 产物丢失时自动重建(方案 3 实现后)
./build.sh tree  # 自动检测并重建

总结

问题本质

Lycium 的增量编译机制依赖 CSV 文件记录,但不验证实际产物:

检查项 当前实现 改进后
CSV 记录 ✅ 检查 ✅ 检查
产物目录存在 ❌ 不检查 ✅ 检查
产物文件存在 ❌ 不检查 ✅ 检查

解决方案对比

方案 优点 缺点 推荐度
方案 1:手动清理 简单快速 需要记住命令 ⭐⭐
方案 2:强制参数 用户控制 每次都要加参数 ⭐⭐⭐
方案 3:智能检查 自动处理 需要修改核心代码 ⭐⭐⭐⭐⭐
方案 4:清理脚本 功能完整 额外维护脚本 ⭐⭐⭐⭐

建议

  1. 短期:使用方案 1(手动清理)+ 方案 4(清理脚本)
  2. 长期:实现方案 3(智能检查)+ 方案 2(强制参数)作为备选

问题类型: 增量编译逻辑缺陷
影响范围: 所有使用 Lycium 构建的库
解决状态: 已提供多种解决方案
推荐方案: 方案 3(智能检查产物)

关键词: Lycium, 增量编译, hpk_build.csv, 产物验证, 构建系统

Logo

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

更多推荐