LLVM CMake 构建系统

CMake 基础知识

这里只列举看懂LLVM CMake文件必要的知识点,详细的文档请参考官方手册1

整体把握

  1. 基于CMake的构建系统被组织为一系列逻辑上的targets,这和Make中的target很类似。所谓的target可以理解为希望构建出的东西,可以是一个执行文件,或者一个库,也可以是一组封装的命令。
    • add_executable可以创建一个二进制的target,会生成对应的执行文件。
    • add_library可以创建一个库的target,会产生对应的库。
    • add_custom_target可创建一个没有输出的target,与makefile中的.PHONY对应,常用于执行一些命令。
  2. 每个有输出的target默认都会加入CMake的all target,也就是说生成makefile后,使用make命令将会构建这些目标。可以通过加入EXCLUDE_FROM_ALL选项来关闭该功能。

  3. 编写CMakeLists.txt使用的是cmake语言,该语言由注释变量命令三部分组成。注释以’#’开始,变量通过set定义,命令则分宏、函数与基本内置命令。变量需要关注的是作用域,cmake只有add_subdirectory与函数调用这唯二的情形会创建新的作用域,每个变量的作用域从当前定义点开始,在其子作用域以及后续的空间发挥作用。子作用域可以重新定义同名变量覆盖父作用域,但是出了作用域又会恢复,除非加入PARENT_SCOPE选项,该选项的作用等同于在该函数调用的下一行定义该变量。以下是一个例子:

    function(modify_test)
      set(test boo PARENT_SCOPE)
      message(${test}) # 输出"foo"
    endfunction()
       
    set(test foo) # 此刻开始test的值是"foo"
    message(${test}) # 输出"foo"
    modify_test()
    # 从此处开始test的值开始变为"boo"
    message(${test}) # 输出"boo"
    

    基本命令是CMake内部实现的,看手册就好;函数是用户自定义的命令,开辟了一个新的作用域;宏与函数类似,唯一的区别在于宏不开辟新的作用域。

  4. Modules是CMake提供的用于支持代码重用的功能。模块也是一段普通的CMake命令,CMake可以通过查找CMAKE_MODULE_PATH指令的目录及其子目录查询对应的模块文件。模块分为查找模块(Find Modules)以及实用模块(Utility Modules)。查找模块支持find_package命令来确定某些软件库或者头文件的位置,提供一些结果变量或者描述函数等;实用模块是一些自定义函数的封装,用于提供一些特定功能。

  5. 可以利用try_compiletry_run命令执行系统环境的检测。基本思路是写一小段测试程序然后用编译器编译或者运行,然后CMake可以根据编译/运行结果置一些变量,以此来判断系统是否支持某些功能。可以参考这篇文档

术语&命令

CMakeLists.txt分析

llvm-project/llvm/CMakeLists.txt为例子进行分析:

  1. 首先定义项目名、项目的版本号以及项目的语言:

    project(LLVM
      VERSION ${LLVM_VERSION_MAJOR}.${LLVM_VERSION_MINOR}.${LLVM_VERSION_PATCH}
      LANGUAGES C CXX ASM)
    

    在我阅读的这个版本中,LLVM_VERSION_MAJORLLVM_VERSION_MINOR以及LLVM_VERSION_PATCH分别被设置为17,0,0。

  2. 包含GNUInstallDirs模块以获取一些在GNU系统下的安装目录:

    include(GNUInstallDirs)
    

    GNUInstallDirs.cmake模块是CMake内置的模块,随CMake的安装而自动安装,可以通过locate GNUInstallDirs命令获取本机上该模块的安装路径。

    $ locate GNUInstallDirs
    ...
    /usr/share/cmake/Modules/GNUInstallDirs.cmake
    

    GNUInstallDirs.cmake的文件头有该模块的说明:

    Define GNU standard installation directories

    Provides install directory variables as defined by the GNU Coding Standards_.

    Result Variables ^^^^^^^^^^^^^^^^

    Inclusion of this module defines the following variables:

    CMAKE_INSTALL_<dir>

    CMAKE_INSTALL_FULL_<dir>

    where <dir> is one of:

    BINDIR user executables (bin) SBINDIR system admin executables (sbin) …

    文件的描述非常清晰,该模块提供了GNU的标准安装路径,并定义了一些结果变量供使用。

  3. 检测CMake命令是否指令了CMAKE_BUILD_TYPE。LLVM要求构建时必须传入-DCMAKE_BUILD_TYPE=<type>,否则构建系统拒绝执行:

       if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
         message(FATAL_ERROR "
       No build type selected. You need to pass -DCMAKE_BUILD_TYPE=<type> in order to configure LLVM.
         ...")
       endif()
    
  4. 检测LLVM_ENABLE_PROJECTSLLVM_ENABLE_RUNTIMES是否是合法的架构与运行时库。

  5. 添加LLVM自定义的模块搜索路径(设置CMAKE_MODULE_PATH),之后即可包含这些目录下的模块:

    list(INSERT CMAKE_MODULE_PATH 0
      "${CMAKE_CURRENT_SOURCE_DIR}/cmake"
      "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules"
      "${LLVM_COMMON_CMAKE_UTILS}/Modules"
      )
    
  6. 包含CPack打包模块。有关CPack的部分还没看,有兴趣的可自行参考相关文档

  7. 定义CMAKE_INSTALL_PACKAGEDIR变量。这个变量的定义在GNUInstallPackageDir模块中,包含该模块即可获取该变量:

    include(GNUInstallPackageDir)
    

    CMAKE_INSTALL_PACKAGEDIR是一个路径变量,变量默认值是lib/cmake。

  8. 定义了一个LLVM_ALL_TARGETS变量表示支持的所有后端架构,如果要新增一个后端架构,可以在其中新增架构名:

     set(LLVM_ALL_TARGETS
       AArch64
       AMDGPU
       ...
       X86
       XCore
       )
    
  9. 定义了一个LLVM_TARGETS_TO_BUILD变量表示需要构建的后端架构,默认是所有架构,也可以在cmake命令行中手动加入-DLLVM_TARGETS_TO_BUILD=<needed targets>来避免所有架构的编译:

     set(LLVM_TARGETS_TO_BUILD "all"
     CACHE STRING "Semicolon-separated list of targets to build, or \"all\".")
        
     if( LLVM_TARGETS_TO_BUILD STREQUAL "all" )
     set( LLVM_TARGETS_TO_BUILD ${LLVM_ALL_TARGETS} )
     endif()
    
  10. 查找FindPython3模块获取组件Interpreter,并将其包含到当前文件:

    find_package(Python3 ${LLVM_MINIMUM_PYTHON_VERSION} REQUIRED
        COMPONENTS Interpreter)
    
    • CMake中的COMPONET表示什么?
  11. 包含模块config-ix实施系统的配置检查,执行LLVM的脚本config.guess获取主机的triple(位于LLVM_HOST_TRIPLE变量中):

    include(config-ix)
    
  12. 设置变量LLVM_TARGET_TRIPLE。除非LLVM_TARGETS_TO_BUILD=native,即native就是target,此时LLVM_TARGET_TRIPLELLVM_NATIVE_TRIPLE一样。否则该变量为空。

    include(SetTargetTriple)
    set_llvm_target_triple()
    
  13. 根据要构建的架构(LLVM_TARGETS_TO_BUILD)配置一些架构相关的变量:

    set(LLVM_ENUM_TARGETS "")
    set(LLVM_ENUM_ASM_PRINTERS "")
    set(LLVM_ENUM_ASM_PARSERS "")
    set(LLVM_ENUM_DISASSEMBLERS "")
    set(LLVM_ENUM_TARGETMCAS "")
    set(LLVM_ENUM_EXEGESIS "")
    foreach(t ${LLVM_TARGETS_TO_BUILD})
      set( td ${LLVM_MAIN_SRC_DIR}/lib/Target/${t} )
        
      list(FIND LLVM_ALL_TARGETS ${t} idx) # 检测LLVM_TARGETS_TO_BUILD中的变量是否合法
      list(FIND LLVM_EXPERIMENTAL_TARGETS_TO_BUILD ${t} idy)
      if( idx LESS 0 AND idy LESS 0 )
        message(FATAL_ERROR "The target `${t}' is experimental and must be passed "
          "via LLVM_EXPERIMENTAL_TARGETS_TO_BUILD.")
      else()
        set(LLVM_ENUM_TARGETS "${LLVM_ENUM_TARGETS}LLVM_TARGET(${t})\n")
      endif()
      ...
      if( EXISTS ${td}/AsmParser/CMakeLists.txt )
        set(LLVM_ENUM_ASM_PARSERS
          "${LLVM_ENUM_ASM_PARSERS}LLVM_ASM_PARSER(${t})\n")
      endif()
      ...
    endforeach()
    

    如果LLVM_TARGETS_TO_BUILD="X86;Mips",那么最后LLVM_ENUM_PARSERS就是:

    LLVM_ASM_PARSER(X86)
    LLVM_ASM_PARSER(Mips)
    

    这些变量会被用于生成头文件:

    configure_file(
      ${LLVM_MAIN_INCLUDE_DIR}/llvm/Config/AsmParsers.def.in
      ${LLVM_INCLUDE_DIR}/llvm/Config/AsmParsers.def
      )
    

    其中AsmParsers.def.in的内容如下:

    #ifndef LLVM_ASM_PARSER
    #  error Please define the macro LLVM_ASM_PARSER(TargetName)
    #endif
        
    @LLVM_ENUM_ASM_PARSERS@
        
    #undef LLVM_ASM_PARSER
    

    展开生成的AsmParsers.def如下:

    #ifndef LLVM_ASM_PARSER
    #  error Please define the macro LLVM_ASM_PARSER(TargetName)
    #endif
        
    LLVM_ASM_PARSER(X86)
    LLVM_ASM_PARSER(Mips)
        
    #undef LLVM_ASM_PARSER
    
  14. 将llvm/include目录与build/include目录加入项目的include目录。

    include_directories( ${LLVM_INCLUDE_DIR} ${LLVM_MAIN_INCLUDE_DIR})
    
  15. 引入LLVM自定义的Module,用于处理子目录的自定义命令与tblgen相关的命令:

    include(AddLLVM)
    include(TableGen)
        
    include(LLVMDistributionSupport)
    
  16. 开始构建子目录,需要保证被依赖的库先构建/定义,否则会出现循环依赖:

    add_subdirectory(lib/Demangle)
    add_subdirectory(lib/Support) # Support依赖Demangle库,所以Demangle最先构建
    add_subdirectory(lib/TableGen) # TableGen依赖Support库,所以紧随其后
        
    add_subdirectory(utils/TableGen)
        
    add_subdirectory(include/llvm)
    add_subdirectory(lib)
    ...
    
  17. 最后执行安装命令,(install命令还没认真看,后面再补上相关介绍,逃~~~

    install(DIRECTORY include/llvm include/llvm-c
      DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
      COMPONENT llvm-headers
      FILES_MATCHING
      PATTERN "*.def"
      PATTERN "*.h"
      PATTERN "*.td"
      PATTERN "*.inc"
      PATTERN "LICENSE.TXT"
      )
                                                                                   
    install(DIRECTORY ${LLVM_INCLUDE_DIR}/llvm ${LLVM_INCLUDE_DIR}/llvm-c
      DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
      COMPONENT llvm-headers
      FILES_MATCHING
      PATTERN "*.def"
      PATTERN "*.h"
      PATTERN "*.gen"
      PATTERN "*.inc"
      # Exclude include/llvm/CMakeFiles/intrinsics_gen.dir, matched by "*.def"
      PATTERN "CMakeFiles" EXCLUDE
      PATTERN "config.h" EXCLUDE
      )
    

注解

  1. 要完全掌握CMake的使用细节需要花不少时间,而且很容易就忘,我的习惯是先大致了解该语言的整体架构,然后遇到不会的内容再去查文档。