美文网首页程序员
[原创] Cmake实战指南

[原创] Cmake实战指南

作者: 赵国开 | 来源:发表于2018-01-04 19:33 被阅读0次

    0 综述


    我觉的Cmake比较核心的一些东西就是

    • 怎么去组织一个项目的编译框架
    • 最终输出目标有哪些(可执行程序,动态库,静态库等等)
    • 怎么为指定的输出目标指定编译参数(需要哪些源文件,需要哪些编译参数)
    • 怎么为指定的输出目标指定链接参数(需要哪些外部库,需要哪些链接参数)
    • 如果存在多个独立输出目标是否有执行先后顺序(比如项目有自动配置工具,用来自动生产一些源文件,那么自动配置工具输出目标就要先于其他目标比如输出可执行程序目标)

    1 如何组织项目编译框架


    比如我当前项目源码根目录下面有这些源码目录

    ├── arch
    ├── cfg
    ├── doc
    ├── drivers
    ├── fs
    ├── include
    ├── kernel
    ├── library
    ├── sample
    └── utils
    

    我会在根目录放置一个 CMakeLists.txt,之后在每个需要管理的目录下面也放置一个CMakeLists.txt,如下所示(这边没有把include包含进来是因为头文件cmake会自动去构建不需要手动添加,拿C语言来举例,所有包含在.C中的.H文件,cmake全部会帮你自动计算出来,不需要手动去添加)

    ├── CMakeLists.txt
    ├── arch
    │   └── CMakeLists.txt
    ├── cfg
    │   └── CMakeLists.txt
    ├── doc
    │   └── CMakeLists.txt
    ├── drivers
    │   └── CMakeLists.txt
    ├── fs
    │   └── CMakeLists.txt
    ├── include
    ├── kernel
    │   └── CMakeLists.txt
    ├── library
    │   └── CMakeLists.txt
    ├── sample
    │   └── CMakeLists.txt
    └── utils
    └── CMakeLists.txt
    ├── cmake
    ├── build
    

    需要说明下这边我多加了两个目录cmake和build,cmake目录一般用来放置一些通用的cmake源码模块(比如通用编译环境设置,编译链接的一些选项配置等等),build用来存放项目构建过程中所有的输出文件。
    根目录下面CMakeLists.txt大致就可以按下面这样撰写,就可以把整个骨架搭起来。

    #cmake最低版本要求
    cmake_minimum_required(VERSION 3.10.0)
    
    #项目名字为test
    project(test NONE)
    
    #包含通用的编译环境模块到顶层目录
    include(${CMAKE_SOURCE_DIR}/cmake/base.cmake)
    
    #下一级的编译目录
    add_subdirectory(arch)
    add_subdirectory(cfg)
    add_subdirectory(doc)
    add_subdirectory(drivers)
    add_subdirectory(fs)
    add_subdirectory(kernel)
    add_subdirectory(library)
    add_subdirectory(sample)
    add_subdirectory(utils)
    

    进入build目录运行整个编译框架

    cd build && cmake .. && make
    

    用到的相关命令

    命令标签格式 说明
    cmake_minimum_required(VERSION major.minor[.patch[.tweak]] [FATAL_ERROR]) 设置最低版本的cmake要求,一般放在第一行
    project(<PROJECT-NAME> [LANGUAGES] [<language-name>...]) 为项目设置名字,版本,启用的编程语言,我上面没有启用任何编程语言所以用NONE
    add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) 增加一个子目录到编译系统
    include(<file|module> [OPTIONAL] [RESULT_VARIABLE <VAR>][NO_POLICY_SCOPE]) 从给出的file文件加载和运行cmake代码

    2 最终输出目标有哪些


    • 最终输出目标就是编译器编译链接之后产生的最终结果,比如linux下面的elf文件,.a静态库,.so的动态库
    • 主要通过add_executable来定义输出可执行程序目标,add_library来定义输出动态库,静态库,模块等目标。
    • 定义的输出目标是全局的,比如你在根目录通过add_executable定义了一个输出目标,可以在子目录中用target_sources命令往这个输出目标里面加源文件,cmake在输出这个目标之前会在当前project中搜集所有添加到该目标的源文件再去生成一个编译链接的规则。
    #定义一个mytool 可执行程序输出目标,目前只有一个源文件  mytool.cpp
    add_executable(mytool mytool.cpp)
    
    #往mytool可执行程序输出目标添加源文件,现在mytool目标里面包含两个源文件
    target_sources(mytool  PRIVATE mytool2.cpp)
    
    #定义一个动态库archive 输出目标,文件有三个源文件
    add_library(archive SHARED archive.cpp zip.cpp lzma.cpp)
    
    #定义一个静态库archive 输出目标,也可以不指定STATIC 因为add_library默认输出目标是#静态库
    add_library(archive STATIC archive.cpp zip.cpp lzma.cpp)
    
    #从给出的源文件直接生成object文件,比如linux下C语言的.o文件
    add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)
    

    用到的相关命令

    命令标签格式 说明
    add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 ...]) 定义一个名字为name可执行程序输出目标,其中 source1 [source2 ...]是源文件,比如C语言是.C的源文件,.H的源文件不需要指定,cmake会自动去搜索
    add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] source1 [source2 ...])

    add_library(<name> OBJECT <src>...)
    定义一个名字为name的库输出目标
    target_sources(<target><INTERFACE|PUBLIC|PRIVATE> [items1...][<INTERFACE|PUBLIC|PRIVATE> [items2...] ...]) 往一个目标里面添加源文件,这个目标名字target是在add_executable() 或者add_library() 中定义的name

    3 指定编译参数和链接参数


    • 为add_executable() 或者add_library() 中定义的输出目标指定编译选项可以通过下面3个命令函数
    命令标签格式 说明
    target_include_directories(<target> [SYSTEM] [BEFORE]<INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...]) Include的头文件的查找目录,也就是Gcc的[-Idir...]选项
    target_compile_definitions(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...][<INTERFACE|PUBLIC|PRIVATE> [items2...] ...]) 通过命令行定义的宏变量,也就是gcc的[-Dmacro[=defn]...]选项
    target_compile_options(<target> [BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...] gcc其他的一些编译选项指定,比如-fPIC
    # gcc头文件查找目录,相当于-I选项,e.g -I/foo/bar
    #CMAKE_SOURCE_DIR是cmake内置变量表示当前项目根目录
    target_include_directories(test_elf
        PRIVATE
        ${CMAKE_SOURCE_DIR}
        ${CMAKE_SOURCE_DIR}/common
        ${CMAKE_SOURCE_DIR}/syscalls
    )
    
    # 编译的宏定义,e.g 相当于-D选项 e.g -Dmacro=defn
    set(MONITOR_OMIT_BSS_INIT      "0")
    set(MONITOR_OMIT_DATA_INIT     "0")
    set(MONITOR_OMIT_T_CHECKS      "0")
    target_compile_definitions(test_elf
        PRIVATE
        MONITOR_OMIT_BSS_INIT=${MONITOR_OMIT_BSS_INIT}              
        MONITOR_OMIT_DATA_INIT=${MONITOR_OMIT_DATA_INIT}            
        MONITOR_TRAP_NT_IRQS=${MONITOR_TRAP_NT_IRQS}                          
    )
    
    # 其他编译选项定义,e.g -fPIC
    target_compile_options(test_elf
        PRIVATE
        -std=c99 
        -Wall 
        -Wextra 
        -Werror
    )
    
    • 为add_executable() 或者add_library() 中定义的输出目标指定链接选项可以通过target_link_libraries命令全部搞定。
    命令标签格式 说明
    target_link_libraries(<target> <PRIVATE|PUBLIC|INTERFACE> <item>... [<PRIVATE|PUBLIC|INTERFACE> <item>...]...) item可以是链接到该目标的库的名字(去掉前缀lib和扩展名后缀之后的库名字),也就是gcc链接器的-llibrary选项

    Item也可以是链接选项以-开始的item会被认为是链接选项(除了 -l和-framework)

    Item也可以是要链接到该目标的库文件的完整路径(针对非标准路径的库时可以这样指定,当然也可以用link_directories命令来为链接器增加搜索库的搜索路径)
    # 链接选项设置
    target_link_libraries(test_elf
        PRIVATE
        -msoft-float 
        -static 
        -nostdlib
    )
    
    #设置链接的标准路径的库
    target_link_libraries(test_elf PRIVATE gcc)
    
    #设置链接的本project自己输出的目标库libkernel.a
    target_link_libraries(test_elf PRIVATE kernel)
    
    #设置链接非标准路径中别人编译好的库
    target_link_libraries(test_elf 
    PRIVATE 
    $ENV{HOME}/lib/libtest.a
    )
    

    备注:也可以用其他方式来设置编译链接的一些参数,比如直接设置cmake内置变量或者其他命令,我这里选择这些基于目标的命令来操作主要是因为,基于目标来管理更容易维护和管理,不会污染到其他的作用域。

    • 下面对命令用到的PRIVATE|PUBLIC|INTERFACE进行介绍一下:
      可以简单理解为向集成自己的其他目标,是否也开放本目标正在设置的这些参数。或者说一个输出目标(自己,比如库),可能会作为另一个目标(依赖者,比如可执行程序)的输入,所以就定义了这几个参数来指明对当前输出目标指定的这些编译参数是否传递给其依赖者,如果你不确定的话,最好都指定为PRIVATE。

    具体的含义如下:

    PRIVATE 只给自己用,不给依赖者用
    PUBLIC 自己和依赖者都可以用
    INTERFACE 自己不用,给依赖着用
    • 下面是cmake官网的一个例子比较简单,自己体会一下
    add_library(archive archive.cpp)
    target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)
    
    add_library(serialization serialization.cpp)
    target_compile_definitions(serialization INTERFACE USING_SERIALIZATION_LIB)
    
    add_library(archiveExtras extras.cpp)
    target_link_libraries(archiveExtras PUBLIC archive)
    target_link_libraries(archiveExtras PRIVATE serialization)
    # archiveExtras is compiled with -DUSING_ARCHIVE_LIB
    # and -DUSING_SERIALIZATION_LIB
    
    add_executable(consumer consumer.cpp)
    # consumer is compiled with -DUSING_ARCHIVE_LIB
    target_link_libraries(consumer archiveExtras)
    

    4 对输出目标的其他处理


    • 比较常见的比如在linux下生成的elf输出目标,需要进一步进行处理从elf文件中抽出bin文件,可以使用add_custom_command命令进行处理,add_custom_command命令支持对目标编译链接之前进行处理,或者编译链接完之后进行处理
      支持该处理方式的add_custom_command命令标签格式如下:
    命令标签格式 说明
    add_custom_command(TARGET <target>
    PRE_BUILD | PRE_LINK | POST_BUILD
    COMMAND command1 [ARGS] [args1...]
    [COMMAND command2 [ARGS] [args2...] ...]
    [BYPRODUCTS [files...]]
    [WORKING_DIRECTORY dir]
    [COMMENT comment]
    [VERBATIM]
    [USES_TERMINAL])
    Target是库或者可执行文件输出目标

    PRE_BUILD只有Visual Studio 8或者之后的版本才支持,其他情况等同于PRE_LINK

    PRE_LINK在编译之后,链接之前运行,

    POST_BUILD,该目标的所有构建规则都执行完之后才运行

    COMMAND ,指定要执行的命令行命令和参数
    #在test_elf链接之前,先拷贝一些test_elf需要用到的文件到编译目录,在进行编译
    add_custom_command(TARGET test_elf PRE_LINK
    COMMAND
    cp ${CMAKE_BINARY_DIR}/cfg/start.o ${CMAKE_BINARY_DIR}/. && 
    cp ${CMAKE_SOURCE_DIR}/target/imx6_gcc/imx6.ld ${CMAKE_BINARY_DIR}/.
    )
    
    #把编译输出的test_elf进行进一步处理,生成最终的bin文件
    add_custom_command(TARGET test_elf POST_BUILD
        COMMAND ${CUSTOM_CMD_OBJCOPY} -O binary -S test_elf test_elf.bin
    )
    

    5 自动生成源码规则制定


    • 如果项目中用到了一些第三方的工具来自动生成一些源码,要怎么制定规则让cmake在编译源码之前先去调用第三方工具先去生成需要编译的源码,主要用到add_custom_target,add_custom_command,dependencies这几个命令
    #先为这个第三方工具设置一个自定义目标,这个目标并没有实际输出文件,
    #只是一个目标的名字比如这边的autoconfig 
    add_custom_target(autoconfig ALL)
    #利用这个目标去运行这个第三方工具,这边是cfg,后面是这个工具需要的参数
    add_custom_command(TARGET autoconfig PRE_BUILD
    COMMAND 
    cfg --pass ${pass} ${CFG_ELF_INCLUDE} ${rom_image} ${symbol_table} ${T_file} ${CFG_TABLES}
    )   
    
    #autoconfig和test_elf这两目标,没有直接的关系(像前面介绍的可执行程序,可能依赖其#他库输出目标,这里没有这样的关系),需要指定目标输出顺序,先对autoconfig进行输
    #出(自动生成源码),在进行test_elf输出(编译和链接)
    add_dependencies(test_elf autoconfig)
    
    #把自动生成的源文件cfg_out.c加到test_elf目标去编译
    target_sources(test_elf  PRIVATE cfg_out.c)
    #指明cfg_out.c是自动生成的文件,否则会认为找不到文件而出错
    set_source_files_properties(cfg_out.c PROPERTIES GENERATED 1)
    
    命令标签格式 说明
    add_custom_target(Name [ALL] [command1 [args1...]] [COMMAND command2[args2...] ...] [DEPENDS depend depend depend ... ] [BYPRODUCTS [files...]] [WORKING_DIRECTORY dir] [COMMENT comment] [VERBATIM] [USES_TERMINAL] [COMMAND_EXPAND_LISTS] [SOURCES src1 [src2...]])[items2...] ...]) 增加一个名字为Name的目标,该目标没有输出,所以它总是被构建。
    ALL选项表明该目标被添加到默认的构建目标,所以它每次都会运行
    add_dependencies(<target> [<target-dependency>]...) 在目标target在构建之前,后面的其他target-dependency依赖目标必须先被构建完成Target或者target-dependency目标是由add_executable(),add_library(), add_custom_target()命令输出的目标
    set_source_files_properties([file1 [file2 [...]]] PROPERTIES prop1 value1 [prop2 value2 [...]]) 通过键/值对( key/value)来设置源文件的一些属性

    6 如何编译汇编源文件


    • 我开发的项目是嵌入式系统有部分源码是用汇编编写的,所以有这个需求。Cmake wiki有介绍了如何编译汇编的介绍,那个可能已经过时了,比较麻烦反正我没试过:)
      我使用下面的方式进行测试,可以正常编译(cmake 版本3.10.0)
    #支持的编程语言配置
    enable_language(ASM)
    
    #设置汇编源文件的编译器,我这边配置成和C语言的编译器一样
    set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER})
    
    #把汇编源码和C源码一起放置在待编译的源文件列表里面就行
    set(SRC_KERNEL_LIB
        ${CMAKE_SOURCE_DIR}/target/imx6_gcc/target_support.S
        ${CMAKE_SOURCE_DIR}/arch/arm_gcc/mpcore/chip_support.S
        ${CMAKE_SOURCE_DIR}/arch/arm_gcc/common/core_support.S
        ${CMAKE_SOURCE_DIR}/drivers/mmu_table.c
    )
    
    命令标签格式 说明
    enable_language(<lang> [OPTIONAL] ) 启用某种编程语言

    7 如何配置cmake的交叉编译环境


    • 这一步按官网交叉编译环境配置说明进行配置就好了,其实就是重写一些变量的值,我项目里面的主要配置如下:
    #配置交叉编译变量
    set(CMAKE_SYSTEM_NAME Linux)
    set(CMAKE_SYSTEM_PROCESSOR arm)
    set(CMAKE_CROSSCOMPILING TRUE)
    #不使用动态链接 -rdyamic
    set(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") 
    
    set(CMAKE_C_COMPILER "$ENV{CROSS_COMPILE_ROOT_PATH}/bin/$ENV{CROSS_COMPILE_PREFIX}-gcc")
    set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER})
    set(CMAKE_AR "$ENV{CROSS_COMPILE_ROOT_PATH}/bin/$ENV{CROSS_COMPILE_PREFIX}-ar")
    set(CMAKE_RANLIB "$ENV{CROSS_COMPILE_ROOT_PATH}/bin/$ENV{CROSS_COMPILE_PREFIX}-ranlib")
    set(CUSTOM_CMD_OBJCOPY "$ENV{CROSS_COMPILE_ROOT_PATH}/bin/$ENV{CROSS_COMPILE_PREFIX}-objcopy")
    set(CUSTOM_CMD_NM "$ENV{CROSS_COMPILE_ROOT_PATH}/bin/$ENV{CROSS_COMPILE_PREFIX}-nm") 
    
    set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
    set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
    set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
    set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
    set(CMAKE_FIND_ROOT_PATH $ENV{CROSS_COMPILE_ROOT_PATH})
    

    8 如何自定义编译器/链接器/归档器


    • 一般不建议去重定义cmake默认的这些配置变量,如果确实默认的一些参数你不需要或者你想扩展出一些自己的定义的话可以考虑。参考模板在
      /usr/local/share/cmake-3.10/Modules/CMakeCInformation.cmake
      (如果你cmake安装是默认路径安装的话,我这边以C语言为例)

    CMakeCInformation.cmake里面的编译器/链接器/归档器的变量定义如下

    #编译C源文件
    if(NOT CMAKE_C_COMPILE_OBJECT)
      set(CMAKE_C_COMPILE_OBJECT
        "<CMAKE_C_COMPILER> <DEFINES> <INCLUDES> <FLAGS> -o <OBJECT>   -c <SOURCE>")
    endif()
    
    #链接C源文件
    if(NOT CMAKE_C_LINK_EXECUTABLE)
      set(CMAKE_C_LINK_EXECUTABLE
        "<CMAKE_C_COMPILER> <FLAGS> <CMAKE_C_LINK_FLAGS> <LINK_FLAGS> <OBJECTS>  -o <TARGET> <LINK_LIBRARIES>")
    endif()
    
    #静态库的链接过程
    if(NOT DEFINED CMAKE_C_ARCHIVE_CREATE)
      set(CMAKE_C_ARCHIVE_CREATE "<CMAKE_AR> qc <TARGET> <LINK_FLAGS> <OBJECTS>")
    endif()
    if(NOT DEFINED CMAKE_C_ARCHIVE_APPEND)
      set(CMAKE_C_ARCHIVE_APPEND "<CMAKE_AR> q  <TARGET> <LINK_FLAGS> <OBJECTS>")
    endif()
    if(NOT DEFINED CMAKE_C_ARCHIVE_FINISH)
      set(CMAKE_C_ARCHIVE_FINISH "<CMAKE_RANLIB> <TARGET>")
    endif()
    
    • 如果你要自定义编译器,链接器,归档器,只需要参考模板,重定义相关的变量就好了。我项目里面编译器和链接器的参数使用cmake默认的配置,但是静态库归档的一些参数cmake默认是写死的,我在我的cmake源码里面进行了重定义。
    #自定义的ar生成规则(cmake默认的ar打包参数和我项目源码定义参数不一样,所以自己重新定义)
    set(CMAKE_STATIC_LINKER_FLAGS "-rcs")
    set(CMAKE_C_ARCHIVE_CREATE "<CMAKE_AR> <LINK_FLAGS> <TARGET> <OBJECTS>")
    

    9 后记


    • 本文用到的大部分示例代码片段,基本上是我项目中在用的一些cmake代码或者cmake官网的示例代码在进行微调。
    • 用到的cmake相关内置命令主要是对一些关键的参数进行介绍,如果想要更详细的介绍可以参考cmake官网的手册。
    • 我也是第一次把cmake引入到具体项目中,本文介绍的一些使用方式,基于我项目需要而去实践的,比如我需要分层管理,独立输出,交叉编译,多种语言混编等等,可能你引入cmake也会有自己的需求和设计。对我来说cmake只是一个工具而已,最本质的那些东西永远是不变的,你要理解你的需求,你要怎么样去设计整个编译框架来满足你的需求,你要用cmake的那些东西来迎合你的设计,你只拿你需要的部分,剩下的那些杂枝细节可以通通扔掉。而不是反过来因为cmake有这个功能,有那个功能,所以设计应该是这样的或者那样的。
    • 我上面介绍的那些实现方式并不是唯一的,我只选择适合我现在项目的。

    参考文献

    [1] https://cmake.org/cmake/help/v3.10/#

    相关文章

      网友评论

        本文标题:[原创] Cmake实战指南

        本文链接:https://www.haomeiwen.com/subject/jvxdnxtx.html