几天前,我想要制作一款C++实现的后端云对接库,预计将要实现Java版类库的完整功能。我准备使用libcurl来完成https请求和证书固定的任务。
在项目配置过程中,我希望项目能在Linux下直接运行,以方便开发过程中的调试工作。又希望正式打包时,能一次性将Android的四个ABI都构建出来,并且包含Android平台的可执行文件。于是,我使用了sh构建脚本。
在该脚本中,手动指定Android NDK的位置和使用的交叉编译工具链,以及编译的目标SDK版本号。在cmake中判断sh传入的环境变量来为特定的平台使用特定的工具链,至此为止一切正常。
使用libcurl需要手动编译libcurl的依赖项,我先是从网络上下载了为Android预编译的二进制文件,可是在cmake中使用时编译器链接总出错,我以为是我的cmake查找路径配置错了,可是AI反复检查我的cmake,给出的建议都是检查查找路径。后终于发现是网络上下载的二进制文件格式不对。使用readelf工具检查后,发现在arm和arm64文件夹下的静态库都是x86格式的。故开始手动编译依赖。
编译过程
要使编译得到的libcurl支持https,那么编译过程中必须引入openssl依赖,而且libcurl还依赖libz。libz编译相对简单,首先编译libz。
环境变量
在编译过程中,需要定义以下环境变量,以便构建工具能找到需要的编译器。
#!/bin/bash
#NDK路径,openssl需要ANDROID_NDK_ROOT变量,所以把它export一下
export ANDROID_NDK_ROOT=$HOME/Library/Android/sdk/ndk/23.1.7779620
#编译平台,我这里是linux,所以是linux-x86_64
HOST_TAG=linux-x86_64
#Android api版本
MIN_SDK_VERSION=23
#工具链路径
TOOLCHAIN=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG
#把工具链加到PATH环境变量
PATH=$TOOLCHAIN/bin:$PATH
#输出目录,在build目录下
BUILD_DIR=$PWD/build
编译libz
解压zlib,并cd到zlib目录下
tar xvf zlib-1.2.13.tar.gz
cd zlib-1.2.13
配置编译工具链,就是配置C/C++编译器、汇编器、链接器的路径,由于Android已经放弃了gcc,现在的编译器是clang和clang++。
要适用于所有CPU架构,我们要编译armv8、armv7、x86、x86_64四个平台。他们会使用不同的clang来编译。我们用TARGET_HOST来区分。 aarch64-linux-android表示要编译armv8 armv7a-linux-androideabi表示要编译armv7a i686-linux-android表示要编译x86 x86_64-linux-android表示要编译x86_64
TARGET_HOST=aarch64-linux-android
ANDROID_ARCH=arm64-v8a
AR=$TOOLCHAIN/bin/llvm-ar
CC=$TOOLCHAIN/bin/$TARGET_HOST$MIN_SDK_VERSION-clang
AS=$CC
CXX=$TOOLCHAIN/bin/$TARGET_HOST$MIN_SDK_VERSION-clang++
LD=$TOOLCHAIN/bin/ld
RANLIB=$TOOLCHAIN/bin/llvm-ranlib
STRIP=$TOOLCHAIN/bin/llvm-strip
配置好工具链后执行configure。–prefix指定编译完成后软件的安装目录。–static只编译静态库。
INSTALL_DIR=$BUILD_DIR/zlib
./configure --prefix=$INSTALL_DIR/$ANDROID_ARCH --static
#上面设置了BUILD_DIR为根目录build文件夹,所以$INSTALL_DIR/$ANDROID_ARCH的值为 build/zlib/arm64-v8a
再执行make、make install。
make
make install
install后,在根目录build/curl下,出现了arm64-v8a。里面就是arm64-v8a版本的zlib库。
我把四个架构的编译写成shell文件。
build-zlib.sh
#!/bin/bash
tar xzf zlib-1.2.13.tar.gz
source ./build-env.sh
INSTALL_DIR=$BUILD_DIR/zlib
if [ ! -d $INSTALL_DIR ]; then
mkdir -p $INSTALL_DIR
fi
cd zlib-1.2.13
function build() {
make distclean
TARGET_HOST=$1
ANDROID_ARCH=$2
AR=$TOOLCHAIN/bin/llvm-ar
CC=$TOOLCHAIN/bin/$TARGET_HOST$MIN_SDK_VERSION-clang
AS=$CC
CXX=$TOOLCHAIN/bin/$TARGET_HOST$MIN_SDK_VERSION-clang++
LD=$TOOLCHAIN/bin/ld
RANLIB=$TOOLCHAIN/bin/llvm-ranlib
STRIP=$TOOLCHAIN/bin/llvm-strip
./configure --prefix=$INSTALL_DIR/$ANDROID_ARCH --static
make -j8
make install
make distclean
}
build aarch64-linux-android arm64-v8a
build armv7a-linux-androideabi armeabi-v7a
build i686-linux-android x86
build x86_64-linux-android x86_64
cd ..
rm -rf cd zlib-1.2.13
编译openssl
通过上面的zlib编译,openssl的编译流程大致是一样的,解压代码什么的就没必要记录了。
有点不一样的是openssl的Configure是大写的。并且它自己支持了Android,需要通过参数传递给它。
然后就是我使用r21的NDK编译时会找不到一些头文件,可能是由于我安装llvm的时候损坏了NDK,换了一个之后就好了。
android-arm64表示编译64位的arm版本。
no-unit-test表示不需要单元测试。
no-shared表示不需要动态库。
-DANDROID_API=$MIN_SDK_VERSION传递Android api版本。
–prefix指定Android目录
./Configure android-arm64 no-unit-test no-shared -D__ANDROID_API__=$MIN_SDK_VERSION --prefix=$INSTALL_DIR/$ANDROID_ARCH
同样写成shell文件
build-openssl.sh
#!/bin/bash
tar xzf openssl-3.0.7.tar.gz
source ./build-env.sh
INSTALL_DIR=$BUILD_DIR/openssl
if [ ! -d $INSTALL_DIR ]; then
mkdir -p $INSTALL_DIR
fi
cd openssl-3.0.7
function build() {
TARGET_HOST=$1
ANDROID_ARCH=$2
OPENSSL_ARCH=$3
AR=$TOOLCHAIN/bin/llvm-ar
CC=$TOOLCHAIN/bin/$TARGET_HOST$MIN_SDK_VERSION-clang
AS=$CC
CXX=$TOOLCHAIN/bin/$TARGET_HOST$MIN_SDK_VERSION-clang++
LD=$TOOLCHAIN/bin/ld
RANLIB=$TOOLCHAIN/bin/llvm-ranlib
STRIP=$TOOLCHAIN/bin/llvm-strip
./Configure $OPENSSL_ARCH no-unit-test no-shared -D__ANDROID_API__=$MIN_SDK_VERSION --prefix=$INSTALL_DIR/$ANDROID_ARCH
make -j8
make install_sw
make distclean
}
build aarch64-linux-android arm64-v8a android-arm64
build armv7a-linux-androideabi armeabi-v7a android-arm
build i686-linux-android x86 android-x86
build x86_64-linux-android x86_64 android-x86_64
cd ..
rm -rf openssl-3.0.7
编译curl
准备完zlib和openssl之后,就可以编译curl了。同样是老套路,configure,make,make install。
–host和–target把编译器平台传给它。
–prefix依旧还是安装目录。
–with-zlib和–with-openssl就用到了我们上面编译出来的zlib和openssl。我们把目录传给它。
–disable-shared表示不编译动态库,我只需要静态库。
./configure --host=$TARGET_HOST \
--target=$TARGET_HOST \
--prefix=$INSTALL_DIR/$ANDROID_ARCH \
--with-zlib=$BUILD_DIR/zlib/$ANDROID_ARCH \
--with-openssl=$BUILD_DIR/openssl/$ANDROID_ARCH \
--disable-shared
curl的完整编译文件如下
build-curl.sh
#!/bin/bash
tar xzf curl-7.88.0.tar.xz
source ./build-env.sh
INSTALL_DIR=$BUILD_DIR/curl
if [ ! -d $INSTALL_DIR ]; then
mkdir -p $INSTALL_DIR
fi
cd curl-7.88.0
function build() {
export TARGET_HOST=$1
export ANDROID_ARCH=$2
export AR=$TOOLCHAIN/bin/llvm-ar
export CC=$TOOLCHAIN/bin/$TARGET_HOST$MIN_SDK_VERSION-clang
export AS=$CC
export CXX=$TOOLCHAIN/bin/$TARGET_HOST$MIN_SDK_VERSION-clang++
export LD=$TOOLCHAIN/bin/ld
export RANLIB=$TOOLCHAIN/bin/llvm-ranlib
export STRIP=$TOOLCHAIN/bin/llvm-strip
./configure --host=$TARGET_HOST \
--target=$TARGET_HOST \
--prefix=$INSTALL_DIR/$ANDROID_ARCH \
--with-zlib=$BUILD_DIR/zlib/$ANDROID_ARCH \
--with-openssl=$BUILD_DIR/openssl/$ANDROID_ARCH \
--with-pic --disable-shared
make -j8
make install
make clean
}
build aarch64-linux-android arm64-v8a
build armv7a-linux-androideabi armeabi-v7a
build i686-linux-android x86
build x86_64-linux-android x86_64
cd ..
rm -rf cd curl-7.88.0
链接问题
在使用上面编译得到的库文件的时候,出现了很多问题。
首先,是stderr,stdout和stdin这三个C语言的标准输入输出链接出错。链接器表示找不到这些符号,但是它们都是C标准库的一部分,应该是默认包含的。网络上给出了一则消息误导了我,我找到一则消息指出Google在NDK r21开始删除了C的标准输入输出,理由是在Android上有特殊的输入输出设备。但是这些内容都是在我使用的库中引用的,我不能直接移除他们。尝试覆盖stderr等定义未果之后,我发现链接libc之后,能解决标准库的链接异常,但是生成的可执行文件会抛出空指针异常,无法运行。而且我注意到x86的架构编译过程失败了。
通过跟踪日志发现,构建x86时,编译工具链查找失败了,是因为x86工具链命名和其他架构不一致,导致找不到编译器。所以我将cmake修改成如下样式:
if (${CMAKE_ANDROID_ARCH_ABI} STREQUAL arm64-v8a)
set(CMAKE_CXX_COMPILER ${CMAKE_ANDROID_NDK}toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android23-clang++)
elseif (${CMAKE_ANDROID_ARCH_ABI} STREQUAL armeabi-v7a)
set(CMAKE_CXX_COMPILER ${CMAKE_ANDROID_NDK}toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi23-clang++)
elseif (${CMAKE_ANDROID_ARCH_ABI} STREQUAL x86)
set(CMAKE_CXX_COMPILER ${CMAKE_ANDROID_NDK}toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android23-clang++)
elseif (${CMAKE_ANDROID_ARCH_ABI} STREQUAL x86_64)
set(CMAKE_CXX_COMPILER ${CMAKE_ANDROID_NDK}toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android23-clang++)
else ()
message(FATAL_ERROR "无法解析的架构:${CMAKE_ANDROID_ARCH_ABI}")
endif ()
这样,cmake可以适应所有的目标ABI。
然后,我发现在我的构建脚本中,通过cmake命令行参数指定了NDK路径和目标API,考虑到这可能会覆盖CMakeLists.txt中的设置,我将它们修改成和CMakeLists.txt中一致之后,标准库的链接问题消失了。
总结
过程中最诡异的问题其实就是最后标准输入输出库链接出错的问题。原因是因为我编译依赖项的时候使用的是NDK r23,目标API是23。而构建脚本中通过命令行参数将NDK指向了r21,目标API也被覆写为21,低于依赖项的版本,导致链接出错。如果在遇到标准库中的内容链接出错,很有可能是使用的工具链和依赖项的工具链不一致,需要仔细检查各项配置。