三方库移植OpenHarmony笔记

2024 年 04 月 19 日

工作需要,把 audit-userspace 2.8.5 移到 OH 4.0 上,记录一下完整的移植过程和自己的理解。

移植工作通常来说并不复杂,但由于移植过程中可能会遇到各种想不到的问题,因此运气成分还是比较大的。如果运气不好的话,就需要处理更多的细节问题。从我个人理解,通用的三方库移植工作按照以下步骤进行。

理解编译过程和输出

所谓编译,就是从源码生成二进制的过程。三方库一般都会简化编译过程到几条命令。对于 audit-userspace,编译命令如下:

# 生成 configure 相关脚本
./autogen.sh
# 编译配置
./configure --without-python --without-python3 --without-golang --without-swig --disable-zos-remote
# 在当前目录执行编译
make
# 注:也可以通过 make DESTDIR=${DEST_DIR} install 将生成文件输出到 DEST_DIR

生成输出之后,可以通过 tree 或 ls 命令分析输出的文件列表。

注:对于简单的工程,也可以直接分析其 Makefile,看是通过哪几个源文件编译得到哪些输出。

理解 cross compile 编译过程

所谓 cross compile(交叉编译),简单说,就是用另外一套工具链,(通常是在 x86/64 上)编译得到适用于目标系统的二进制文件。

通常来说,要移植的工程如果是跨平台的,一般都会为目标平台做一些准备工作,以简化移植过程。一般用 configure --help 就能看到相关的帮助信息。audit-userspace 的 configure 就提供了类似说明。适用的 configure 命令参考如下:

./configure --host=arm-linux-gnueabi --with-arm --with-aarch64 --without-python --without-python3 --without-golang --without-swig --disable-zos-remote

在这条命令里,通过 --host 指定了目标主机,通过 --with-arm 和 --with-aarch64 显式指出了目标平台。

但是,在这种情况下,工具链限制适用 arm-linux-gnueabi。如果想要使用另外一套工具链,则需要指定一系列环境变量。例如:

export AS=${CLANG_DIR}/llvm-as
export CC="${CLANG_DIR}/clang"
export CXX="${CLANG_DIR}/clang++"
export LD="${CLANG_DIR}/ld.lld"
export STRIP=${CLANG_DIR}/llvm-strip
export RANLIB=${CLANG_DIR}/llvm-ranlib
export OBJDUMP=${CLANG_DIR}/llvm-objdump
export OBJCOPY=${CLANG_DIR}/llvm-objcopy
export NM=${CLANG_DIR}/llvm-nm
export AR=${CLANG_DIR}/llvm-ar

上面的脚本指定了一系列工具链相关的环境变量。个别情况下,可以加上参数。

可以通过 file 命令检查输出文件的信息是否符合需求。

可以通过 nm 或 strings 命令检查工具链所用到的库的符号表,如果遇到链接问题的话可能会有帮助。

理解 OH 编译系统

OH 编译系统使用 GN & Ninja。前者将 gn 文件生成 ninja 文件。后者执行 ninja 文件中的编译过程。

从层级上来看,OH 编译系统将系统模组从高到低分为以下几个层级:

  • subsystem,子系统,包含一系列组件,其配置文件位于 build/subsystem_config.json
  • component,组件,通常一个工程就是一个组件。组件目录下会带一个 bundle.json 来描述组件相关信息。
  • build target,编译目标,一个组件下可能有多个编译目标。通常会在组件目录下有一到多个 BUILD.gn 文件,每个文件里面都可以有多个编译目标。

要让 OH 识别到新增的 build target,以我现在了解的情况,有两种办法:

  1. 通过一个已有的工程,加入新的编译目标的依赖,然后在新的工程目录下增加 BUILD.gn 文件,包括新的编译目标,让 OH 编译系统识别。
  2. 将工程转化为组件,即为工程增加 BUILD.gnbundle.json,如果没有对应的 subsystem,则还需要修改 subsystem 配置文件。

另外,关于编译目标,OH 编译系统加入了一系列模板来简化编译。具体可以参考这篇文档

理解工程整合过程

关于第三方库工程的整合,实际上就是用 OH 编译系统,对原有的工程做 cross compile,然后将输出的文件,复制到目标路径。

对于 audit-userspace 2.8.5,下面是一个裁剪过的 BUILD.gn 文件。实现了几个编译目标。

### BUILD.gn

# 引入 ohos 提供的模板和变量
import("//build/ohos.gni")

# 编译动作
action("build_audit_userspace") {
  script = "build_audit_userspace.sh" # 要执行的脚本
  outputs = [ # 下面是所需要的输出文件,只保留了两个,其他省略
    "${target_out_dir}/usr/local/lib/libaudit.so.1.0.0",
    "${target_out_dir}/usr/local/sbin/auditd",
  ]
  args = [ # 要传递给脚本的参数,主要是将相对路径转换为绝对路径
    rebase_path("."),
    rebase_path("${target_out_dir}"),
    rebase_path("${root_build_dir}"),
    rebase_path("//prebuilts/clang/ohos/linux-x86_64/llvm/bin"),
    rebase_path("//prebuilts/gcc/linux-x86/aarch64/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin"),
    "${device_name}"
  ]
}

# 安装 libaudit.so,安装依赖于 build_audit_userspace 目标的输出
ohos_prebuilt_shared_library("install_audit_libaudit") {
  source = "${target_out_dir}/usr/local/lib/libaudit.so.1.0.0"
  output = "libaudit.so.1"
  subsystem_name = "thirdparty"
  part_name = "audit-userspace"
  deps = [":build_audit_userspace"]
}


# 安装 auditd,安装依赖于 build_audit_userspace 目标的输出
ohos_prebuilt_executable("install_audit_auditd") {
  source = "${target_out_dir}/usr/local/sbin/auditd"
  output = "auditd"
  install_enable = true
  subsystem_name = "thirdparty"
  part_name = "audit-userspace"
  deps = [":build_audit_userspace"]
}

# 其他文件的安装动作省略

# 将所有文件的安装合并为一个编译目标
group("install_audit_userspace") {
  deps = [
    ":install_audit_libaudit",
    ":install_audit_auditd",
  ]
}

上面的 gn 文件将编译系统的相关参数传递给 build_audit_userspace.sh 脚本,由脚本执行编译动作。然后再将编译生成的文件复制到目标路径。

编译脚本参考如下:

### build_audit_userspace.sh

# 接收输入的路径参数,转为常量定义
SRC_DIR=${1}
TARGET_OUT_DIR=${2}
ROOT_BUILD_DIR=${3}
CLANG_DIR=${4}
LINARO_DIR=${5}

# 打印路径信息,方便调试
echo "Building audit"
echo "SRC_DIR=${SRC_DIR}"
echo "TARGET_OUT_DIR=${TARGET_OUT_DIR}"
echo "ROOT_BUILD_DIR=${ROOT_BUILD_DIR}"
echo "CLANG_DIR=${CLANG_DIR}"
echo "LINARO_DIR=${LINARO_DIR}"

# 指定编译工具链
export AS=${CLANG_DIR}/llvm-as
export CC="${CLANG_DIR}/clang --target=arm-linux-ohos"
export CXX="${CLANG_DIR}/clang++ -std=c++17"
export LD="${CLANG_DIR}/ld.lld --target=arm-linux-ohos"
export STRIP=${CLANG_DIR}/llvm-strip
export RANLIB=${CLANG_DIR}/llvm-ranlib
export OBJDUMP=${CLANG_DIR}/llvm-objdump
export OBJCOPY=${CLANG_DIR}/llvm-objcopy
export NM=${CLANG_DIR}/llvm-nm
export AR=${CLANG_DIR}/llvm-ar

# 调整编译参数
unset CFLAGS
unset CPPFLAGS
unset LDFLAGS
export CFLAGS="-fPIC --target=arm-linux-ohos -march=armv7-a -mfloat-abi=softfp -D__MUSL__=1 --sysroot=${ROOT_BUILD_DIR}/obj/third_party/musl -Wno-int-conversion"
export CXXFLAGS="-fPIC --target=arm-linux-ohos -march=armv7-a -mfloat-abi=softfp -D__MUSL__=1 --sysroot=${ROOT_BUILD_DIR}/obj/third_party/musl -Wno-int-conversion"
export LDFLAGS="--target=arm-linux-ohos -march=armv7-a -mfloat-abi=softfp --sysroot=${ROOT_BUILD_DIR}/obj/third_party/musl --rtlib=compiler-rt -fuse-ld=lld -L${ROOT_BUILD_DIR}/obj/third_party/musl/usr/lib/arm-linux-ohos"

# 进入源码目录,开始编译
cd ${SRC_DIR}
# 生成 configure 文件
./autogen.sh
# 清理文件,这一步非必须
make clean -s
# 配置编译环境
./configure --host=arm-linux --with-arm --with-aarch64 --without-python --without-python3 --without-golang --without-swig --disable-zos-remote --with-sysroot=${ROOT_BUILD_DIR}/obj/third_party/musl
# 执行编译,输出到目标路径
make DESTDIR=${TARGET_OUT_DIR} install

即使一切正确,由于工具链的问题,编译过程中也可能会遇到各种错误。这时候就必须根据报错修改代码,将一些无法链接的函数替换或补充为其他实现,比如 rawmemchrstrndupa 函数,还有未做 extern 的 event_node_list 变量。

注:如果是一些比较简单的库,可能直接写 BUILD.gn 进行编译会比较容易,不一定非得要做脚本文件。

Top