CMake入门

1. CMake介绍

CMake一般用来构建大型项目,为了弄清整个项目的结构,源码在哪里,生成的产物在哪里等等,了解CMake以后以上问题都会迎刃而解。

1.1 构建

一般来说,为了方便管理,避免弄乱项目结构,我们会新一个build目录去存放生成的产物。如果不新建build会生成很多杂乱的文件,相关文件,可以参考第二节

在这里插入图片描述

上面执行的过程如下

  1. 书写CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(CALC)
add_executable(app add.c div.c main.c mult.c sub.c)
  • cmake_minimum_required:指定使用的 cmake 的最低版本 可选,非必须,如果不加可能会有警告

  • project:定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定出工程名字即可。可选项很多, 我们一般写一个工程名就行了

  • add_executable:定义工程会生成一个可执行程序

    add_executable(可执行程序名 源文件名称)
    

    这里的可执行程序名和project中的项目名没有任何关系,有多个源文件,可以用空格或;间隔

  1. 执行cmake命令,一般就是cmake . cmake ..

    # cmake 命令原型
    $ cmake CMakeLists.txt文件所在路径
    
  2. 执行make

    当执行cmake命令以后,会根据CMakeLists.txt生成一些文件,其中就有makefile文件,然后再执行make命令进行构建,就能得到可执行程序。

根据前面的测试可以发现,生成的文件跟源码混在一起,比较混乱,所以为了方便管理,一般都新建一个build目录去存放生成产物。这时候在build目录下执行cmake ..

1.2 CMake常用命令
定义变量
# SET 指令的语法是:
# VAR表示变量名称,VALUE表示变量的值,可以为多个,后面一些可选项,用到的时候查文档即可
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])

# 方式1: 各个源文件之间使用空格间隔
# set(SRC_LIST add.c  div.c   main.c  mult.c  sub.c)

# 方式2: 各个源文件之间使用分号 ; 间隔
set(SRC_LIST add.c;div.c;main.c;mult.c;sub.c)
add_executable(app  ${SRC_LIST})
指定使用的C++标准

gcc/g++编译的时候我们一般这样写

g++ *.cpp -std=c++11 -o app

上面的例子中通过参数-std=c++11指定出要使用c++11标准编译程序,C++标准对应有一宏叫做DCMAKE_CXX_STANDARD。在CMake中想要指定C++标准有两种方式:

  • 在CMakeLists.txt中通过set指定:set(CMAKE_CXX_STANDARD 11)
  • 在执行cmake的时候设置宏的值:cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=11
指定输出路径

同上面一样,在CMake中指定可执行程序输出的路径,也对应一个宏,叫做EXECUTABLE_OUTPUT_PATH,它的值还是通过set命令进行设置:

set(HOME /home/robin/Linux/Sort)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)

如果这个路径中的子目录不存在,会自动生成,无需自己手动创建

搜索文件

一般来说有两种方式,aux_source_directory命令或者file命令

1、aux_source_directory

aux_source_directory(< dir > < variable >)

# 搜索 src 目录下的文件,存在SRC_LIST变量中
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)

2、file

file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
  • GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
  • GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
# 搜索当前目录的src目录下所有的源文件,并存储到变量中
file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB MAIN_HEAD ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
# 关于要搜索的文件路径和类型可加双引号,也可不加:
file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")
包含头文件

在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在CMake中设置要包含的目录也很简单,通过一个命令就可以搞定了,他就是include_directories:

include_directories(${PROJECT_SOURCE_DIR}/include)

PROJECT_SOURCE_DIR宏对应的值就是我们在使用cmake命令时,后面紧跟的目录,一般是工程的根目录。

1.3 制作静态库或动态库
  • 静态库

    add_library(库名称 STATIC 源文件1 [源文件2] ...) 
    
  • 动态库

    add_library(库名称 SHARED 源文件1 [源文件2] ...) 
    
1.4 指定输出路径

在Linux中生成的动态库,默认是有可执行权限的,静态库默认不具有可执行权限。

  • 针对动态库

    cmake_minimum_required(VERSION 3.0)
    project(CALC)
    include_directories(${PROJECT_SOURCE_DIR}/include)
    file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
    # 设置动态库生成路径
    set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
    add_library(calc SHARED ${SRC_LIST})
    

    对于这种方式来说,其实就是通过set命令给EXECUTABLE_OUTPUT_PATH宏设置了一个路径,这个路径就是可执行文件生成的路径

  • 都适用,设置LIBRARY_OUTPUT_PATH

    cmake_minimum_required(VERSION 3.0)
    project(CALC)
    include_directories(${PROJECT_SOURCE_DIR}/include)
    file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
    # 设置动态库/静态库生成路径
    set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
    # 生成动态库
    #add_library(calc SHARED ${SRC_LIST})
    # 生成静态库
    add_library(calc STATIC ${SRC_LIST})
    

如果只是单纯的生成库,这两种方式足以,但其实还有几个宏,可以指定生成路径,可参考2.2 存在的问题

1.5 链接库文件
  • 静态库

    link_libraries(<static lib> [<static lib>...])
    
    # 链接静态库
    link_libraries(calc)
    

    如果是自定义的库,一般还需指明一下库的路径

    cmake_minimum_required(VERSION 3.0)
    project(CALC)
    # 搜索指定目录下源文件
    file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
    # 包含头文件路径
    include_directories(${PROJECT_SOURCE_DIR}/include)
    # 包含静态库路径
    link_directories(${PROJECT_SOURCE_DIR}/lib)
    # 链接静态库
    link_libraries(calc)
    add_executable(app ${SRC_LIST})
    
  • 动态库

    target_link_libraries(
        <target> 
        <PRIVATE|PUBLIC|INTERFACE> <item>... 
        [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
    
    
    • target:指定要加载动态库的文件的名字

      • 该文件可能是一个源文件

      • 该文件可能是一个动态库文件

      • 该文件可能是一个可执行文件

    • PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为PUBLIC

      • 如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可。

      • 动态库的链接具有传递性,如果动态库 A 链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了动态库B、C,并可以使用动态库B、C中定义的方法。

        target_link_libraries(A B C)
        target_link_libraries(D A)
        
        • PUBLIC:在public后面的库会被Link到前面的target中,并且里面的符号也会被导出,提供给第三方使用
        • PRIVATE:在private后面的库仅被Link到前面的target中,并且终结掉,第三方不能感知你调了啥库
        • INTERFACE:在interface后面引入的库不会被链接到前面的target中,只会导出符号

这里需要注意的是,动态库的加载和静态库的加载是不同的。静态库在生成可执行程序的时候,就被打包到了可执行程序中,可执行程序一启动就被加载到内存中,而动态库只有在库函数被调用的时候,才会加载到内存中。

因此我们链接动态库是先生成可执行程序,再链接,而静态库则是先链接!

cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 添加并指定最终生成的可执行程序名
add_executable(app ${SRC_LIST})
# 指定可执行程序要链接的动态库名字
target_link_libraries(app pthread)

在上面的例子中,用link_libraries链接静态库,使用target_link_libraries命令链接动态库,但事实上,target_link_libraries也可以链接静态库文件。

1.6 常见的宏⭐️
  • CMAKE_BINARY_DIR:项目实际构建路径,也就是cmake命令执行的路径,比如/build目录

  • CMAKE_ARCHIVE_OUTPUT_DIRECTORY:默认存放静态库的文件夹位置;

  • CMAKE_LIBRARY_OUTPUT_DIRECTORY:默认存放动态库的文件夹位置;

  • LIBRARY_OUTPUT_PATH默认存放库文件的位置,如果产生的是静态库并且没有指定 CMAKE_ARCHIVE_OUTPUT_DIRECTORY 则存放在该目录下,动态库也类似;

  • CMAKE_RUNTIME_OUTPUT_DIRECTORY:存放可执行软件的目录;

  • PROJECT_SOURCE_DIR:在使用cmake命令时,后面紧跟的目录,一般是工程的根目录;

  • CMAKE_SOURCE_DIR: 最外层CMakeLists.txt所在目录;

  • CMAKE_CURRENT_SOURCE_DIR:当前正在处理的源目录的路径;

2. 嵌套的CMake⭐️

如果项目很大,或者项目中有很多的源码目录,在通过CMake管理项目的时候如果只使用一个CMakeLists.txt,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个CMakeLists.txt文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。

先来看一下下面的这个的目录结构:

$ tree
.
├── build
├── calc
│   ├── add.cpp
│   ├── CMakeLists.txt
│   ├── div.cpp
│   ├── mult.cpp
│   └── sub.cpp
├── CMakeLists.txt
├── include
│   ├── calc.h
│   └── sort.h
├── sort
│   ├── CMakeLists.txt
│   ├── insert.cpp
│   └── select.cpp
├── test1
│   ├── calc.cpp
│   └── CMakeLists.txt
└── test2
    ├── CMakeLists.txt
    └── sort.cpp

6 directories, 15 files

  • include 目录:头文件目录
  • calc 目录:目录中的四个源文件对应的加、减、乘、除算法
    对应的头文件是include中的calc.h
  • sort 目录 :目录中的两个源文件对应的是插入排序和选择排序算法
    对应的头文件是include中的sort.h
  • test1 目录:测试目录,对加、减、乘、除算法进行测试
  • test2 目录:测试目录,对排序算法进行测试

可以看到各个源文件目录所需要的CMakeLists.txt文件现在已经添加完毕了。接下来庖丁解牛,我们依次分析一下各个文件中需要添加的内容。

2.1 准备工作
节点关系

众所周知,Linux的目录是树状结构,所以嵌套的 CMake 也是一个树状结构,最顶层的 CMakeLists.txt 是根节点,其次都是子节点。因此,我们需要了解一些关于 CMakeLists.txt 文件变量作用域的一些信息:

  • 根节点CMakeLists.txt中的变量全局有效
  • 父节点CMakeLists.txt中的变量可以在子节点中使用
  • 子节点CMakeLists.txt中的变量只能在当前节点中使用
添加子目录
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
  • source_dir:指定了CMakeLists.txt源文件和代码文件的位置,其实就是指定子目录
  • binary_dir:指定了输出文件的路径,一般不需要指定,忽略即可。
  • EXCLUDE_FROM_ALL:在子路径下的目标默认不会被包含到父路径的ALL目标里,并且也会被排除在IDE工程文件之外。用户必须显式构建在子路径下的目标。

通过这种方式CMakeLists.txt文件之间的父子关系就被构建出来了。

2.2 解决问题

在上面的目录中我们要做如下事情:

  • 通过 test1 目录中的测试文件进行计算器相关的测试
  • 通过 test2 目录中的测试文件进行排序相关的测试

现在相当于是要进行模块化测试,对于calcsort目录中的源文件来说,可以将它们先编译成库文件(可以是静态库也可以是动态库)然后在提供给测试文件使用即可。库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式。

一般来说,这里要么是把源文件编译成一个可执行文件,要么就是编译成一个库,然后在搞一个test目录!

根目录🚀

根目录中的 CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.0)
project(test)
# 定义变量
# 静态库生成的路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
# 测试程序生成的路径
set(EXEC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
# 头文件目录
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 静态库的名字
set(CALC_LIB calc)
set(SORT_LIB sort)
# 可执行程序的名字
set(APP_NAME_1 test1)
set(APP_NAME_2 test2)
# 添加子目录
add_subdirectory(calc)
add_subdirectory(sort)
add_subdirectory(test1)
add_subdirectory(test2)

在根节点对应的文件中主要做了两件事情:定义全局变量和添加子目录。

  • 定义的全局变量主要是给子节点使用,目的是为了提高子节点中的CMakeLists.txt文件的可读性和可维护性,避免冗余并降低出差的概率。

  • 一共添加了四个子目录,每个子目录中都有一个CMakeLists.txt文件,这样它们的父子关系就被确定下来了。

calc目录

calc 目录中的 CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.0)
project(CALCLIB)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(${CALC_LIB} STATIC ${SRC})
  • 第3行aux_source_directory:搜索当前目录(calc目录)下的所有源文件
  • 第4行include_directories:包含头文件路径,HEAD_PATH是在根节点文件中定义的
  • 第5行set:设置库的生成的路径,LIB_PATH是在根节点文件中定义的
  • 第6行add_library:生成静态库,静态库名字CALC_LIB是在根节点文件中定义的
sort目录

sort 目录中的 CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.0)
project(SORTLIB)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(${SORT_LIB} SHARED ${SRC})

第6行add_library:生成动态库,动态库名字SORT_LIB是在根节点文件中定义的

这个文件中的内容和calc节点文件中的内容类似,只不过这次生成的是动态库。

在生成库文件的时候,这个库可以是静态库也可以是动态库,一般需要根据实际情况来确定。如果生成的库比较大,建议将其制作成动态库。

test1目录

test1 目录中的 CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.0)
project(CALCTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
link_directories(${LIB_PATH})
link_libraries(${CALC_LIB})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
add_executable(${APP_NAME_1} ${SRC})
  • 第4行include_directories:指定头文件路径,HEAD_PATH变量是在根节点文件中定义的
  • 第6行link_libraries:指定可执行程序要链接的静态库,CALC_LIB变量是在根节点文件中定义的
  • 第7行set:指定可执行程序生成的路径,EXEC_PATH变量是在根节点文件中定义的
  • 第8行add_executable:生成可执行程序,APP_NAME_1变量是在根节点文件中定义的

此处的可执行程序链接的是静态库,最终静态库会被打包到可执行程序中,可执行程序启动之后,静态库也就随之被加载到内存中了。

test2目录

test2 目录中的 CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.0)
project(SORTTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
link_directories(${LIB_PATH})
add_executable(${APP_NAME_2} ${SRC})
target_link_libraries(${APP_NAME_2} ${SORT_LIB})
  • 第四行include_directories:包含头文件路径,HEAD_PATH变量是在根节点文件中定义的
  • 第五行set:指定可执行程序生成的路径,EXEC_PATH变量是在根节点文件中定义的
  • 第六行link_directories:指定可执行程序要链接的动态库的路径,LIB_PATH变量是在根节点文件中定义的
  • 第七行add_executable:生成可执行程序,APP_NAME_2变量是在根节点文件中定义的
  • 第八行target_link_libraries:指定可执行程序要链接的动态库的名字

在生成可执行程序的时候,动态库不会被打包到可执行程序内部。当可执行程序启动之后动态库也不会被加载到内存,只有可执行程序调用了动态库中的函数的时候,动态库才会被加载到内存中,且多个进程可以共用内存中的同一个动态库,所以动态库又叫共享库。

构建项目

一切准备就绪之后,开始构建项目,进入到根节点目录的build 目录中,执行cmake 命令,如下:

$ cmake ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/cmake/calc/build

可以看到在build目录中生成了一些文件和目录,如下所示:

$ tree build -L 1     
build
├── calc                  # 目录
├── CMakeCache.txt        # 文件
├── CMakeFiles            # 目录
├── cmake_install.cmake   # 文件
├── Makefile              # 文件
├── sort                  # 目录
├── test1                 # 目录
└── test2                 # 目录

然后在build 目录下执行make 命令:

  • 在项目根目录的lib目录中生成了静态库libcalc.a
  • 在项目根目录的lib目录中生成了动态库libsort.so
  • 在项目根目录的bin目录中生成了可执行程序test1
  • 在项目根目录的bin目录中生成了可执行程序test2

最后再来看一下上面提到的这些文件是否真的被生成到对应的目录中了:

$ tree bin/ lib/
bin/
├── test1
└── test2
lib/
├── libcalc.a
└── libsort.so

在这里插入图片描述

存在的问题⭐️

但是上面的方式还是有一点问题,因为我们在根目录的CMakeLists.txt中定义了

# 静态库生成的路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
# 测试程序生成的路径
set(EXEC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)

所以最终生成的库和执行文件是在根目录下的libbin目录,有没有什么方式可以让生成的文件全部在build目录下的?

  • CMAKE_BINARY_DIR:项目实际构建路径,也就是cmake命令执行的路径,比如/build目录

  • CMAKE_ARCHIVE_OUTPUT_DIRECTORY:默认存放静态库的文件夹位置;

  • CMAKE_LIBRARY_OUTPUT_DIRECTORY:默认存放动态库的文件夹位置;

  • LIBRARY_OUTPUT_PATH默认存放库文件的位置,如果产生的是静态库并且没有指定 CMAKE_ARCHIVE_OUTPUT_DIRECTORY 则存放在该目录下,动态库也类似;

  • CMAKE_RUNTIME_OUTPUT_DIRECTORY:存放可执行软件的目录;

了解了上面五个宏以后,我们就可以这样修改,删除根目录下定义的库文件和可执行文件的定义,子目录也相应删掉。根目录添加

# 设置可执行文件和库的输出目录

# 可执行文件
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# 动态库
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# 库
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib)

这样整个目录结构就很清晰了!

在这里插入图片描述

3. CMake生成器

CMake默认的生成器是,Unix Makefiles,也就是make,其实也可以换成Ninja,Ninja更快速。

在这里插入图片描述

只需要cmake .. - G Ninja,然后ninjaninja install即可

makemake install一样,前者是编译生成对应的库、可执行文件等,后者会根据在CMakeLists.txt中的定义,将这些库、可执行文件,安装到你指定的路径中!

CMakeLists.txt内容为

cmake_minimum_required(VERSION 3.0)
project(CALC)
add_executable(app add.c div.c main.c mult.c sub.c)

# 下面就是指定CMAKE_INSTALL_PREFIX安装路径,可执行文件安装在它的bin子目录下
set(CMAKE_INSTALL_PREFIX "/home/jia/code/test/build")
message(${CMAKE_INSTALL_PREFIX})
install(TARGETS app DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)

在这里插入图片描述

根据上图可以清晰的看到,cmake .. -G Ninja ninja ninja install后的每一步输出,ninja对应的构建文件是build.ninja,对应make的Makefile。

  • CMakeCache.txt里面放了一些缓存变量

  • cmake_install.cmake里面放了一些install的规则,比如复制、设置权限等,当你运行make install或者ninja install的时候,会自动执行里面的命令,默认的CMAKE_INSTALL_PREFIX “/usr/local”,想要执行这个文件,可以cmake -P cmake_install.cmake

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值