第22.4章:LLVM:构建与宿主

English

1. Clang 没有"目标的构建"

GCC 针对一个目标配置。用 --target=aarch64-linux-gnu 构建出来的 gcc 只生成 AArch64,别的都不行。LLVM 是另一种设计:一个代码生成器带多个后端, 全部编进一个 clang 二进制。目标在调用时选择:

clang++ --target=aarch64-linux-gnu --sysroot=$SYSROOT ...
clang++ --target=x86_64-windows-gnu --sysroot=$MINGW ...
clang++ --target=wasm32-wasip1 --sysroot=$WASI ...

所以 GCC 意义上的"目标的 clang"不存在。存在的是一个运行在宿主上、包含 LLVM_TARGETS_TO_BUILD 所启用后端的 clang。只要 AArch64 后端在,同一个二进制就能 在 x86_64 机器上、AArch64 机器上或者 Windows 机器上生成 AArch64 代码。交叉编译不是另一次构建, 而是同一个本地驱动换一个 --target。

但编译器总得在某个地方运行。那就是宿主。因此 LLVM 的构建只有两个三元组,不是三个:

概念 在 LLVM/CMake 里 含义
构建 运行 CMake/Ninja 的机器 cmake 和 ninja 在哪里运行
宿主 CMAKE_C_COMPILER_TARGET、LLVM_HOST_TRIPLE 产出的 clang 二进制在哪里运行
默认目标 LLVM_DEFAULT_TARGET_TRIPLE 用户不传 --target 时 clang 生成什么
启用的后端 LLVM_TARGETS_TO_BUILD 这个二进制能为哪些指令集生成代码

最容易弄错的是 CMAKE_C_COMPILER_TARGET 和 LLVM_DEFAULT_TARGET_TRIPLE 的区别。前者告诉"构建 clang 的那个编译器"为某个三元组 生成代码,所以 clang 二进制本身就是那个三元组的。后者只改 clang 的默认值,二进制仍然 运行在构建机上。想要后者却设了前者,得到的是一个在你机器上不能执行的编译器。

因为目标是调用参数,clang 只有两个构建类别,二者都由构建和宿主决定:

类别 关系 含义
本地(Native) build = host clang 二进制在哪里构建就在哪里运行。交叉编译属于这一类:同一个本地 clang 加 --target= 和 --sysroot= 调用。
加拿大(Canadian) build ≠ host clang 二进制在另一台机器上生产,运行在宿主上。

clang 没有交叉构建。22.1 里 GCC 的交叉行存在,是因为 GCC 在配置期固定目标;clang 不固定。交叉是本地 clang 的一种用法, 所以是本地编译的特例。加拿大本地/加拿大交叉/反向编译的划分也不适用:目标不是 clang 构建的 一部分,所以加拿大 clang 就是加拿大编译。

2. 什么算一次加拿大 LLVM 构建

假设你在 x86_64-linux-gnu 上,想要一个运行在 aarch64-linux-gnu 上、 能生成 AArch64 的 clang。你像交叉编译任何 C++ 程序一样交叉编译 clang:使用的编译器必须生成 AArch64,系统根必须是 AArch64 的 C/C++ 系统根,因为 clang 要链接 libc 和 C++ 运行时。 这就是加拿大构建:构建是 x86_64,宿主是 aarch64。这是 clang 唯一的非本地类别。目标根本不进入 CMake 的三元组;它是后端和默认值,之后由 --target 选择。

如果你想要一个运行在构建机上、但默认面向 AArch64 的 clang,就不要设 CMAKE_C_COMPILER_TARGET。让二进制保持本地,设 LLVM_DEFAULT_TARGET_TRIPLE=aarch64-linux-gnu,并确保 LLVM_TARGETS_TO_BUILD 包含 AArch64。这是 LLVM 里最接近 GCC 交叉编译器 的东西,它只是一个多带一个后端的本地 clang。当你再传 --target=aarch64-linux-gnu --sysroot=$SYSROOT 时,就是在用这个本地 clang 交叉编译;编译器二进制没有变。

两种情况下,构建都需要同样三样实际的东西:

3. CMake 配置

用 CMake toolchain 文件表达宿主和系统根是最干净的方式。参考脚本的形态是:

set(CMAKE_BUILD_TYPE Release)
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_ASM_COMPILER clang)

# The produced clang runs on this triple, so clang emits its code for it.
set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_ASM_COMPILER_TARGET aarch64-linux-gnu)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_CROSSCOMPILING On)

set(CMAKE_SYSROOT /home/you/toolchains/llvm/aarch64-linux-gnu/aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

运行 CMake 测试程序的编译器是构建机上的 clang,但 CMAKE_C_COMPILER_TARGET 让它产出 AArch64 二进制。CMake 本身运行在构建机上; 它构建出来的目标文件属于宿主。这就是一次加拿大构建,只是没有 --host 参数。

然后用宿主三元组、默认目标和本地工具配置 LLVM:

cmake -G Ninja -S llvm-project/llvm -B build-aarch64 \
  -DCMAKE_TOOLCHAIN_FILE=$PWD/aarch64.cmake \
  -DCMAKE_INSTALL_PREFIX=/home/you/toolchains/llvm/aarch64-linux-gnu/llvm \
  -DLLVM_HOST_TRIPLE=aarch64-linux-gnu \
  -DLLVM_DEFAULT_TARGET_TRIPLE=aarch64-linux-gnu \
  -DLLVM_TARGETS_TO_BUILD="AArch64;X86" \
  -DLLVM_NATIVE_TOOL_DIR=/home/you/toolchains/llvm/native/bin \
  -DLLVM_ENABLE_PROJECTS="clang;lld;clang-tools-extra" \
  -DLLVM_ENABLE_ZLIB=FORCE_ON \
  -DZLIB_LIBRARY=$SYSROOT/usr/lib/libz.a \
  -DZLIB_INCLUDE_DIR=$SYSROOT/usr/include \
  -DBUILD_SHARED_LIBS=ON \
  -DLLVM_ENABLE_LTO=Thin \
  -DLLVM_ENABLE_TERMINFO=OFF \
  -DLLVM_ENABLE_LIBXML2=OFF \
  -DLLVM_INCLUDE_TESTS=OFF \
  -DLLVM_INCLUDE_BENCHMARKS=OFF \
  -DLLVM_ENABLE_ASSERTIONS=OFF

ninja -C build-aarch64
ninja -C build-aarch64 install/strip

CMake 有时会在第一次配置时忽略新的 toolchain 文件。参考脚本因此把同一条 cmake 命令跑两遍;第二次相同的调用很便宜,可以避免"它用了宿主编译器"这种令人困惑的失败。

4. 本地工具:真正会咬人的地方

LLVM 在构建时用 llvm-tblgen 和 clang-tblgen 生成很大一部分自己的源码。 这些程序必须运行在构建机上。交叉编译出来的 clang 在那里跑不了,所以 CMake 需要一份本地副本。

现代做法是 LLVM_NATIVE_TOOL_DIR,由 D131052 引入,2023 年合入。把它指向本地 LLVM 构建的 bin 目录,CMake 就会使用那些可执行文件,而不是自己再构建:

-DLLVM_NATIVE_TOOL_DIR=/home/you/toolchains/llvm/native/bin

旧的说明会逐个设置工具:

-DLLVM_TABLEGEN=/path/to/native/bin/llvm-tblgen
-DCLANG_TABLEGEN=/path/to/native/bin/clang-tblgen
-DLLVM_CONFIG_PATH=/path/to/native/bin/llvm-config

两种都行。要点是 ninja install 默认不安装 llvm-tblgen 和 clang-tblgen,所以"先安装 clang,再用这个安装去交叉构建"的天真做法会因为找不到 TableGen 而失败。保留本地构建目录,或者把工具拷到一个稳定的位置。

5. 运行时库

"clang 编译器"和"完整的 clang 工具链"是两回事。编译器本身可以使用目标系统根里 GCC 的 libstdc++ 和 libgcc。这是得到可用交叉编译器最快的方式:

$PREFIX/bin/clang++ --target=$TARGET \
  --gcc-toolchain=$GCC_PREFIX \
  --sysroot=$SYSROOT \
  hello.cc -o hello

自包含的 LLVM 工具链还会带上 compiler-rt、libc++、 libc++abi 和 libunwind。把它们作为 runtime 构建,使用刚构建好的 clang 和目标系统根:

cmake -G Ninja -S llvm-project/runtimes -B build-runtimes \
  -DCMAKE_C_COMPILER=$PREFIX/bin/clang \
  -DCMAKE_CXX_COMPILER=$PREFIX/bin/clang++ \
  -DCMAKE_C_COMPILER_TARGET=$TARGET \
  -DCMAKE_CXX_COMPILER_TARGET=$TARGET \
  -DCMAKE_SYSROOT=$SYSROOT \
  -DCMAKE_INSTALL_PREFIX=$PREFIX \
  -DLLVM_ENABLE_RUNTIMES="compiler-rt;libcxx;libcxxabi;libunwind" \
  -DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=ON

ninja -C build-runtimes
ninja -C build-runtimes install

构建顺序很重要:compiler-rt 的 builtins 不依赖 C++ 库;libunwind 需要 C++ 头文件但不需要 C++ 库;libc++abi 需要 libunwind; libc++ 需要 libc++abi。如果你只需要 C,跳过这些,直接用系统根的 libc。

运行时装好之后,显式选择它们:

clang++ --target=$TARGET --sysroot=$SYSROOT \
  --rtlib=compiler-rt --stdlib=libc++ \
  -nostdinc++ -isystem $PREFIX/include/c++/v1 \
  -L$PREFIX/lib -Wl,-rpath,$PREFIX/lib \
  hello.cc -o hello

6. 实际会遇到的失败

症状 原因 修复
构建期间 llvm-tblgen: not found 没有本地工具;加拿大构建试图在构建机上运行宿主二进制 LLVM_NATIVE_TOOL_DIR 或 LLVM_TABLEGEN/CLANG_TABLEGEN
产出的 clang 在构建机上不能执行 想要本地 clang,却把 CMAKE_C_COMPILER_TARGET 设成了目标 去掉它,或者把结果当作宿主二进制并送到宿主
libstdc++.so.6: version GLIBCXX_3.4.xx not found 宿主的 C++ 运行时比构建 clang 用的更旧 LLVM_STATIC_LINK_CXX_STDLIB=ON,或随产物带新运行时,或设 LD_LIBRARY_PATH
cannot find -lz,或者压缩支持被静默关闭 没有找到目标的 zlib LLVM_ENABLE_ZLIB=FORCE_ON 并显式给 ZLIB_LIBRARY/ZLIB_INCLUDE_DIR
宿主库泄漏进构建 pkg-config 和 CMake 在宿主上搜索 CMAKE_FIND_ROOT_PATH、PKG_CONFIG_LIBDIR、PKG_CONFIG_PATH=""
链接时报缺少 crtbegin.o 或 -lc 没有目标系统根,或者布局不对 读 22.2,检查 -print-search-dirs
构建想要 Python、curses 或 libxml2 默认启用了可选项目 关掉 lldb,用 LLVM_ENABLE_TERMINFO=OFF、LLVM_ENABLE_LIBXML2=OFF

BUILD_SHARED_LIBS=ON 是一个有意的取舍。它让 LLVM 库变成动态库,减小安装体积, 让 clang 工具共享它们,但意味着宿主必须有兼容的 libstdc++.so.6 或 libc++.so。如果你要发布到无法控制的机器上,要么用 -DLLVM_STATIC_LINK_CXX_STDLIB=ON 构建,要么把运行时一起打包并设置 rpath。 不要因为宿主的 C 编译器能用,就假设宿主的 C++ 运行时足够新。

7. 验证结果

$PREFIX/bin/clang --version
$PREFIX/bin/clang -print-target-triple
$PREFIX/bin/clang -print-targets        # enabled backends
$PREFIX/bin/clang -dumpmachine

$PREFIX/bin/clang --target=$TARGET --sysroot=$SYSROOT \
  -v -E -x c /dev/null                  # include search path

file $PREFIX/bin/clang                  # must match the host, not the build machine

最后一项检查专门抓走错的加拿大构建。如果 file 说 ELF 64-bit LSB ... x86-64,而你想要 AArch64 宿主,那就是构建用了本地编译器, 而不是面向目标的编译器。编译器仍然能运行,也仍然能编译 hello world,所以这个失败能躲过一次 随意的测试。打包之前,把工具链拷到目标宿主上,在那里运行 clang --version。

这一页的参考实现是 trcrsired/toolchainbuildscripts 里的 canadian/canadian_clang_aarch64-linux-gnu.txt 和 llvm/build_common.sh。那些文件把 CMAKE_C_COMPILER_TARGET、 LLVM_HOST_TRIPLE、LLVM_DEFAULT_TARGET_TRIPLE、系统根、zlib 和本地 TableGen 工具放在一处;读完这一页,这些选项名应该读起来像对构建过程的描述,而不是一串参数。