背景

我们有时候写一些基础性类库或者实验新功能的时候,常常需要使用到最新版本的GCC和Clang。一些Linux发行版的源里和一些工具链(比如MSYS2)里其实自带LLVM套件的包,LLVM 官网也提供一些常见平台的预编译包下载。 那为什么我们还要自己编译呢?如果有注意到的小伙伴可能会发现,很多平台的源和 LLVM 官网 里下载的预编译包,其实是缺失很多组件的。有些没有libc++和libc++abi(CentOS 8),有些没有Sanitizer相关的组件,有些缺失其他的组件。而Clang虽然支持GCC的libstdc++,但是一方面我们写基础性类库还是要优先考虑原生STL库的兼容性,另一方面Clang对libstdc++的支持也不是太好,特别是有些第三方库在这个组合下也是没有适配得很好,同时gdb和libc++的搭配有时候也不是很完善。 所以我们就需要一个组件尽可能开完整地包含LLVM,Clang,libc++,libc++abi还有其他周边工具(各类Sanitizer,clang-tiny,clang-analyzer等等)的工具链。

之前其实我一致维护有一个脚本 LLVM&Clang Installer 用来编译这些工具链,并且一致更新到了现在的 12.0 。然而早期的时候 LLVM 自带的流程里还不包含自举编译的流程,而且时不时它的脚本适配也会出现问题。所以这个脚本流程主要是下载和编译一些依赖组件并且完成第一次编译和第二次的自举流程。但是现在 LLVM 官方给出了 “All In One” 的源码包仓库 ,里面 clang/cmake/caches 里包含了一些编译的设置文件,其中也包含自举编译的设置。(甚至也包含stage 3的验证流程,就像GCC的编译流程是第一遍普通编译,第二遍自举编译,第三遍还有一次验证编译,即编译完和第二步自举编译的结果进行对比)。

我之前的脚本 LLVM&Clang Installer 其实很早就切到了 “All In One” 的源码包仓库 。但是一直没切到官方的自举编译流程。这次则是切到官方的自举编译流程,并且记录下踩得一些坑。

依赖库

这个 “All In One” 的源码包仓库 之所以打上引号,也是由于其实它并不是包含所有的依赖包。比如 lldb 依赖的 python , libedit , 和一些通用的依赖像 swig , zlib 等等。 而 python 又依赖 libffi (否则无法编译内置模块 _ctypes , 这个模块被很多库所依赖。 )和 openssl 等等。

对于 openssl , 我懒得再写一遍编译流程了,于是直接复用之前写的 GCC 构建脚本 GCC Installer 流程里的版本。因为现在的 LLVM 编译对 GCC版本要求还是很高的。另外还有一些基础性质的工具比如 libtool, pkg-config 等等也是复用了 GCC Installer 流程里的了。

其他的库像 libeditswig 等就需要单独编译。但是这些哭没有再做自举编译,也是因为都是C库,自举的意义不大。

Bootstrap编译

关于自举编译,可以参考 https://github.com/llvm/llvm-project/blob/main/llvm/docs/AdvancedBuilds.rst 这里的文档。但是其实这里写得相当模糊。具体的用法还是得结合源码和Example里来看。文档里提及的设置文件其实也是缺失很多组件的,和官方网站提供的预编译包一样。要开启更多的功能,还是得自己额外开自己试设置组合。

大体的流程就是首先在 stage 1的设置文件里要开启Bootstrap编译:

set(CLANG_ENABLE_BOOTSTRAP ON CACHE BOOL "")
set(CLANG_BOOTSTRAP_EXTRA_DEPS builtins runtimes CACHE STRING "")

然后可以指定Stage 2自举编译时的设置文件

set(CLANG_BOOTSTRAP_CMAKE_ARGS -C ${CMAKE_CURRENT_LIST_DIR}/distribution-stage2.cmake CACHE STRING "")

对于第二阶段自举编译传入的参数,可以通过上面的配置文件指定。也可以通过最外层执行 cmake 时使用带 BOOTSTRAP_ 前缀的参数指定。 比如我们如果使用自己编译的GCC来进行Stage 1阶段编译,为了保证Stage 1和Stage 2查找的GCC一致,可以通过 cmake [...] -DBOOTSTRAP_CMAKE_CXX_FLAGS=--gcc-toolchain=$GCC_TOOLCHAIN -DBOOTSTRAP_CMAKE_C_FLAGS=--gcc-toolchain=$GCC_TOOLCHAIN 来指定Stage 2阶段增加 CMAKE_CXX_FLAGS=--gcc-toolchain=$GCC_TOOLCHAIN CMAKE_C_FLAGS=--gcc-toolchain=$GCC_TOOLCHAIN 。来手动指定GCC Toolchain的目录。

然后,在Stage 1的设置文件里要给 CLANG_BOOTSTRAP_TARGETS 里包含 install-distributioninstall-distribution-stripped (我尝试过和Example一样加 install-distribution-toolchain 的话会编不过,如果有知道为啥的小伙伴欢迎解答一下)。接下来,在Stage 2的设置文件里 set(LLVM_DISTRIBUTION_COMPONENTS 要安装的组件列表) 就可以了。接下来就是要精心选择需要的组件集合。

LLVM_ENABLE_PROJECTS , LLVM_ENABLE_RUNTIMESLLVM_DISTRIBUTION_COMPONENTS

LLVM 的构建系统里分为 LLVM_ENABLE_PROJECTSLLVM_ENABLE_RUNTIMES 两组配置。一些组件可以在 LLVM_ENABLE_PROJECTSLLVM_ENABLE_RUNTIMES 里任选其一。有些只能在 LLVM_ENABLE_PROJECTS 里,同样有一些组件只能在 LLVM_ENABLE_RUNTIMES 里。那些可以在 LLVM_ENABLE_PROJECTSLLVM_ENABLE_RUNTIMES 里任选其一的组件中,配在哪里会影响是否参与自举编译和是否install(因为我们最终是通过 cmake --build . --config $BUILD_TYPE --target stage2 stage2-distribution 来安装需要的组件的)。这些组件不能在两边同时存在,否则会生成多个同名的target。

至于最终 LLVM_ENABLE_PROJECTSLLVM_ENABLE_RUNTIMES 里放哪些组件,其实取决于最重要安装的内容的依赖关系。经过多次测试,我最终的设置如下:

Stage 1中

set(LLVM_ENABLE_PROJECTS "clang;clang-tools-extra;lld;llvm;lldb;libclc;parallel-libs;pstl" CACHE STRING "")

set(LLVM_TARGETS_TO_BUILD Native CACHE STRING "") # X86;ARM;AArch64;RISCV

Stage 2中

set(LLVM_ENABLE_PROJECTS "clang;clang-tools-extra;lld;llvm;lldb;libclc;parallel-libs;pstl" CACHE STRING "")
set(LLVM_ENABLE_RUNTIMES "compiler-rt;libcxx;libcxxabi;libunwind" CACHE STRING "")

set(LLVM_TARGETS_TO_BUILD Native CACHE STRING "") # X86;ARM;AArch64;RISCV

LLVM_DISTRIBUTION_COMPONENTS

最终安装的时候需要install哪些组件是写在 LLVM_DISTRIBUTION_COMPONENTS 里面的。官方给的Example的设置里都缺失一些我需要的组件。但是相对来说 Fuchsia-stage2.cmake 的设置相对来说比较全面。所以我最终使用的设置就是在 Fuchsia-stage2.cmake 的基础上,增加了 llvm-as , llvm-addr2line , llvm-addr2line , llvm-config , llvm-elfabi , llvm-install-name-tool , llvm-jitlink , llvm-lto , llvm-lto2 , llvm-ml , llvm-pdbutil , LLVM , LTO , Remarks , lldb 及相关工具 , libclang 及相关库和头文件, clang-check , clang-cpp , clang-libraries , scan-build , scan-view , pp-trace , modularize , opt-viewer 。基本上就是补充了一些常用工具和这些工具的运行时依赖,比如说各类 Santinizer 好像是在 clang-libraries 这个 Component 里。

设置缓存

因为大部分我们在配置文件里写的代码都是 set(LLVM_ENABLE_LIBCXX ON CACHE BOOL "") 这种形式。这种形式有个问题是如果之前缓存存在的话就用之前的设置,否则才会用我们设置的值。而在有自举编译的情况下,LLVM 里的组件是一个一个编译的,导致有些Stage 1的设置会被传递到Stage 2里来。那么为了解决这个问题,LLVM的构建系统允许我们通过指定特定目标架构的配置来覆盖默认配置。这样我们就可以在Stage 2里通过指定目标平台的设置来强制复写Stage 1阶段的设置。

在我的设置文件里,我通过

foreach(target aarch64-unknown-linux-gnu;armv7-unknown-linux-gnueabihf;i386-unknown-linux-gnu;x86_64-unknown-linux-gnu)
  if(LINUX_${target}_SYSROOT OR target STREQUAL "${LINUX_NATIVE_TARGET}")
    set(BUILTINS_<KEY> <VALUE> CACHE STRING "")
    set(RUNTIMES_${target}_<KEY> <VALUE> CACHE STRING "")
  endif()
endforeach()

来设置平台特定的设置。其中在x86_64的Linux下会把 LINUX_NATIVE_TARGET 设为 x86_64-unknown-linux-gnu

最终成果

除了上面提到的 -DBOOTSTRAP_CMAKE_CXX_FLAGS=--gcc-toolchain=$GCC_TOOLCHAIN -DBOOTSTRAP_CMAKE_C_FLAGS=--gcc-toolchain=$GCC_TOOLCHAIN 外。我其实还设置了一些配置透传,主要是某些组件的复用和根据编译机设置的复用。(LLVM的编译太耗内存了)。大致上就是 -DCLANG_BOOTSTRAP_PASSTHROUGH=CMAKE_INSTALL_PREFIX;CMAKE_FIND_ROOT_PATH;CMAKE_PREFIX_PATH;LLVM_PARALLEL_LINK_JOBS;PYTHON_HOME;LLDB_PYTHON_VERSION;LLDB_ENABLE_PYTHON;LLDB_RELOCATABLE_PYTHON

构建脚本开源到了 https://github.com/owent-utils/bash-shell/blob/main/LLVM%26Clang%20Installer/12.0/installer-bootstrap.sh

Stage 1的配置在 https://github.com/owent-utils/bash-shell/blob/main/LLVM%26Clang%20Installer/12.0/distribution-stage1.cmake

Stage 2的配置在 https://github.com/owent-utils/bash-shell/blob/main/LLVM%26Clang%20Installer/12.0/distribution-stage2.cmake

以后大版本变化可以在 https://github.com/owent-utils/bash-shell/tree/main/LLVM%26Clang%20Installer 里找最新版本。

LLVM 构建流程的文档实在糟糕,也欢迎有兴趣的小伙伴们一起互相交流。