cmake-CMake记录

测试工程: https://github.com/yangxuan0261/CMakeLab


前篇


CMake编译原理

CMake 是一种跨平台编译工具,比 make 更为高级,使用起来要方便得多。CMake 主要是编写 CMakeLists.txt 文件,然后用 cmake 命令将 CMakeLists.txt 文件转化为 make 所需要的 makefile 文件,最后用 make 命令编译源码生成可执行程序或共享库(so(shared object))。因此 CMake 的编译基本就两个步骤:

  1. cmake
  2. make

CMake说明

一般把 CMakeLists.txt 文件放在工程目录下,使用时,先创建一个叫 build 的文件夹(这个并非必须,因为 cmake 命令指向CMakeLists.txt 所在的目录,例如 cmake .. 表示 CMakeLists.txt 在当前目录的上一级目录。cmake 后会生成很多编译的中间文件以及 makefile 文件,所以一般建议新建一个新的目录,专门用来编译),然后执行下列操作:

1
2
3
cd build 
cmake ..
make

其中 cmake .. 在build里生成 Makefile,make根据生成 makefile 文件,编译程序,make 应当在有 Makefile 的目录下,根据 Makefile 生成可执行文件。


最简单 CMakeLists.txt 文件

最简单的一个工程需要有一个这样的 CMakeLists.txt 文件

1
2
3
cmake_minimum_required(VERSION 3.15)
project(Tutorial)
add_executable(Tutorial tutorial.cxx) // 或者 库: add_library(Tool01 SHARED library.cpp library.h)

ubuntu 安装 cmake

可以使用 apt install cmake, 不过 18.04 安装的 cmake 版本是 3.10.2.

安装指定版本

  1. 去 GitHub 上下载指定的版本, 如: cmake-3.15.0.tar.gz

  2. 解压 并 编译

    1
    2
    3
    4
    5
    $ tar -xvzf cmake-3.15.0.tar.gz
    $ cd cd cmake-3.15.0
    $ ./bootstrap #执行引导文件, 该命令执行需要一定时间,请耐心等待。成功执行结束之后,末尾提示:CMake has bootstrapped. Now run make.
    $ make
    $ sudo make install

    生成的可执行文件都在 bin 目录下

  3. 拷贝 可执行文件 到系统目录

    1
    2
    3
    $ cp bin/* /usr/bin/
    $ cmake --version # 查看版本
    cmake version 3.15.0
  4. 清理安装源代码

    1
    2
    $ cd ..
    $ rm -fr cmake-3.15.0

常用的预定义变量

PROJECT_NAME: 通过PROJECT指定的项目名称

1
project(Demo)

PROJECT_SOURCE_DIR: 工程的根目录,上图中的Demo目录
PROJECT_BINARY_DIR: 执行cmake命令的目录,一般是在build目录,在此目录执行cmake ..
CMAKE_CURRENT_SOURCE_DIR: 当前CMakeLists.txt文件所在目录
CMAKE_CURRENT_BINARY_DIR: 编译目录,可使用ADD_SUBDIRECTORY来修改此变量

1
2
# 添加cmake执行子目录
ADD_SUBDIRECTORY(example)

EXECUTABLE_OUTPUT_PATH: 二进制可执行文件输出位置

1
2
# 设置可执行文件的输出路径为 build/bin
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

LIBRARY_OUTPUT_PATH: 库文件输出位置

1
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

常用系统信息变量

1
2
$ cmake --version
cmake version 3.11.2

CMAKE_MAJOR_VERSION: cmake 的 主版本号cmake version 3.11.2中的3
CMAKE_MINOR_VERSION: cmake 的 次版本号cmake version 3.11.2中的11
CMAKE_PATCH_VERSION: cmake 的 补丁等级cmake version 3.11.2中的2

CMAKE_SYSTEM: 系统名称,带版本号
CMAKE_SYSTEM_NAME: 系统名称,不带版本号
CMAKE_SYSTEM_VERSION: 系统版本号
CMAKE_SYSTEM_PROCESSOR: 处理器名称


编译选项

BUILD_SHARED_LIBS: 默认的库编译方式(shared or static),默认为static,一般在 ADD_LIBRARY时直接指定编译库的类型
CMAKE_C_FLAGS: 设置C编译选项
CMAKE_CXX_FLAGS: 设置C++编译选项

  • CMAKE_CXX_FLAGS_DEBUG: 设置编译类型为 Debug 时的编译选项
  • CMAKE_CXX_FLAGS_RELEASE: 设置编译类型为 Release 时的编译选项

CMAKE_CXX_COMPILER: 设置C++编译器

1
2
3
4
5
6
7
8
9
10
set (CMAKE_BUILD_TYPE "Debug")

# 设置C++编译器为g++
set(CMAKE_CXX_COMPILER "g++")
# 设置标准库版本为c++17 并开启警告
set(CMAKE_CXX_FLAGS "-std=c++17 -Wall")
# 设置Debug模式下,不开启优化,开启调试,生成更详细的gdb调试信息
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -Wall -g -ggdb")
# 设置Release模式下,开启最高级优化
set(CMAKE_CXX_FLAGS_RELEASE "-O3")

语法


拷贝文件 单个

语法:

1
2
3
4
configure_file(<input> <output>
[COPYONLY] [ESCAPE_QUOTES] [@ONLY]
[NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ]
)

示例

1
2
3
4
5
6
# 拷贝文件
configure_file(
"${LINK_DIR}/libTool01.dll"
"${PROJECT_BINARY_DIR}/libTool01.dll"
COPYONLY
)

拷贝文件 批量

1
2
3
4
5
6
7
set(libDstPath "${LINK_DIR}")
file(GLOB libFiles # 匹配到需要拷贝的文件
"${INC_DIR}/cmake-build-debug/lib*"
)
message(STATUS "--- libFiles: ${libFiles}")
message(STATUS "--- libDstPath: ${libDstPath}")
file(COPY ${libFiles} DESTINATION ${libDstPath})

设置导出名称

1
set_target_properties(tool01 PROPERTIES OUTPUT_NAME "hello") # 修改导出的库名. 将原来导出为 libtool01.so 的库名修改为 libhello.so

添加子目录

可以参考: 有源码

作用是: 可以加入含有 CMakeLists.txt 的子目录, 一起构建, 可以理解为 有源码的库

语法:

1
ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

例如

1
2
PROJECT(HELLO)
ADD_SUBDIRECTORY(src bin)

构建完后, src 生成的中间文件会在 执行命令时所在的目录的 bin 目录下


添加编译选项

CMAKE_CXX_FLAGS 内置变量拼接一下即可, 如: 拼接上 -lmingw32

g++ 增加 -std=c++0x 参数

1
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -lmingw32")

glob 匹配文件 - 批量导入

如果每加一个 .h/.cpp 文件都要在 add_executable 编译中加入, 未免有点麻烦

可以使用 glob 去匹配文件实现 批量导入


非递归 GLOB

1
2
3
4
5
6
7
8
# set(SOURCE_FILES main.cpp test.cpp test.h) # 指定文件

file(GLOB SOURCES_H_CPP # glob 匹配导入根目录文件, 不能递归子目录
*.h
*.cpp
)
set(SOURCE_FILES ${SOURCES_H_CPP})
add_executable(TestCpp ${SOURCE_FILES})

递归 GLOB_RECURSE

1
2
3
4
5
6
file(GLOB_RECURSE SRC_aaa # 递归 aaa 目录及子目录 中的所有 h/cpp 文件
aaa/**.h
aaa/**.cpp
)
set(SOURCE_FILES main.cpp ${SRC_aaa} )
add_executable(TestCpp ${SOURCE_FILES})

添加 预编译宏

1
2
3
4
5
# 添加 预编译宏
add_definitions(
-D USE_LOG=3 # 定义值
-D USE_FILTER
)

cpp 中使用

1
2
3
4
5
6
7
8
#if (USE_LOG == 3)
std::cout << USE_LOG << std::endl;
std::cout << "USE_LOG yes! " << std::endl;
#endif

#ifdef USE_FILTER
std::cout << "USE_FILTER yes!" << std::endl;
#endif

添加 第三方库

无源码

这种方式适用于 第三方库 没有源码, 只有 头文件库文件

  1. 编写根目录 CMakeLists.txt 文件

    1. 引入 头文件 路径 和 动态库 路径, 位置要在 add_executable 之前

      1
      2
      3
      4
      5
      set(INC_DIR E:/ws_cpp/Tool)
      set(LINK_DIR E:/ws_cpp/Tool/cmake-build-debug)

      include_directories(${INC_DIR})
      link_directories(${LINK_DIR})

      这里引入动态库文件是 libTool.dll, 所以这里写 Tool (lib的写法就是: lib[name].dll )

    2. 连接库, 位置要在 add_executable 之后

      1
      target_link_libraries(TestCpp Tool)
  2. build 一下.

    1
    2
    3
    "D:\CLion 2019.3.3\bin\cmake\win\bin\cmake.exe" --build E:\ws_cpp\TestCpp\cmake-build-debug --target TestCpp -- -j 6
    [ 33%] Linking CXX executable TestCpp.exe
    [100%] Built target TestCpp

    Linux 环境下的编译

    1
    2
    3
    4
    $ mkdir build # 新建一个 build 临时目录用来存放临时生产的 中间文件
    $ cd build
    $ cmake .. # .. 是指 CMakeLists.txt 文件所在目录, 这里也就是上一级目录
    $ make # 可执行程序就出来了

    构建完执行 TestCpp.exe 会报错: Process finished with exit code -1073741515 (0xC0000135), 原因是找不到 libTool.dll, 可以通过两种方式解决:

    1. 需要将 libTool.dll 丢到与 TestCpp.exe 同一目录.
    2. libTool.dll 所在的目录加入环境变量.

附:

  • 完整 CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        cmake_minimum_required(VERSION 3.15)
    project(TestCpp)

    set(CMAKE_CXX_STANDARD 14)

    set(SOURCE_FILES main.cpp test.cpp test.h)

    set(INC_DIR E:/ws_cpp/Tool)
    set(LINK_DIR E:/ws_cpp/TestCpp/cmake-build-debug)

    include_directories(${INC_DIR}) # 相当于gcc/clang 中的-I(i的大写字母)参数, 引入 头文件 目录
    link_directories(${LINK_DIR}) # 相当于gcc 中的-L参数, 引入 库文件 目录

add_executable(TestCpp ${SOURCE_FILES})

target_link_libraries(TestCpp Tool) # 相当于gcc中的-l(小写的l)参数, 链接 库文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17



---

#### 有源码

这种方式适用于 第三方库 有源码, 只有 *头文件* 和 *cpp文件*

一般可视为项目的其他模块, 连同其他模块一起编译, 比如: 项目根目录下有个模块 *Tool02*



1. 编写模块 *Tool02* 的 CMakeLists.txt 文件.

```json
add_library(Tool02 library.cpp)
  1. 编写根目录 CMakeLists.txt 文件

    1. 引入 头文件 路径 和 模块目录 Tool02

      1
      2
      include_directories ("${PROJECT_SOURCE_DIR}/Tool02")
      add_subdirectory (Tool02)
    2. 连接库, 位置要在 add_executable 之后

      1
      target_link_libraries(TestCpp Tool02)
  2. build 一下.

    1
    2
    3
    "D:\CLion 2019.3.3\bin\cmake\win\bin\cmake.exe" --build E:\ws_cpp\TestCpp\cmake-build-debug --target TestCpp -- -j 6
    [ 33%] Linking CXX executable TestCpp.exe
    [100%] Built target TestCpp

附:

  • 完整 CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    cmake_minimum_required(VERSION 3.15)
    project(TestCpp)

    set(CMAKE_CXX_STANDARD 14)

    set(SOURCE_FILES main.cpp test.cpp test.h)

    include_directories ("${PROJECT_SOURCE_DIR}/Tool02")
    add_subdirectory (Tool02)

    add_executable(TestCpp ${SOURCE_FILES})

    target_link_libraries(TestCpp Tool02)

动态库 静态库

语法:

1
ADD_LIBRARY(hello [SHARED|STATIC|MODULE][EXCLUDE_FROM_ALL]source1 source2 ... sourceN)

你不需要写全libhello.so,只需要填写hello即可,cmake系统会自动为你生成libhello.X。类型有三种:

  • SHARED,动态库(扩展名为.so)
  • STATIC,静态库(扩展名为.a)
  • MODULE,在使用dyld的系统有效,如果不支持dyld,则被当作SHARED对待。
  • EXCLUDE_FROM_ALL 参数的意思是这个库不会被默认构建,除非有其他的组件依赖或者手工构建。

库 版本号

语法:

1
SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)

VERSION指代动态库版本,SOVERSION指代API版本。将上述指令加入lib/CMakeLists.txt中,重新构建看看结果。

在build/lib目录会生成:

1
2
3
libhello.so.1.2
libhello.so.1->libhello.so.1.2
libhello.so ->libhello.so.1

安装 共享库/头文件

将libhello.a, libhello.so.x以及hello.h安装到系统目录,才能真正让其他人开发使用,在本例中我们将hello的共享库安装到/lib目录,将hello.h安装到/include/hello目录。

CMakeLists.txt中添加如下指令:

1
2
3
4
5
INSTALL(TARGETS hello hello1 hello2
LIBRARY DESTINATION lib # 动态库目的目录
ARCHIVE DESTINATION libstatic) # 静态库目的目录

INSTALL(FILES hello.h DESTINATION include/hello) # 文件的目录

注意,静态库要使用 ARCHIVE 关键字通过:

1
2
3
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
make
make install

就可以将头文件和共享库安装到系统目录 /usr/lib 和 /usr/include/hello 中了。


debug/release 构建

两种方式都可以

  1. cmake 命令时指定

    1
    2
    3
    4
    mkdir Release  
    cd Release
    cmake -DCMAKE_BUILD_TYPE=Release .. # Release or Debug
    make
  2. CMakeLists.txt 文件中指定

    1
    set(CMAKE_BUILD_TYPE "Release") # Release or Debug

clion 中添加构建版本. 参考: c++_编辑器CLion.md 中的 debug/release 构建


安装

这里需要引入一个新的 cmake 指令 INSTALL和一个非常有用的变量 CMAKE_INSTALL_PREFIX

指定该值的两种方式

  1. CMAKE_INSTALL_PREFIX 变量类似于 configure 脚本的 –prefix,可以在 cmake 编译时指定变量值

    1
    cmake -D CMAKE_INSTALL_PREFIX=/usr .
  2. CMakeLists.txt 中指定

    1
    set(CMAKE_INSTALL_PREFIX "/usr")

CMAKE_INSTALL_PREFIX 的默认定义是 /usr/local

INSTALL 指令用于定义安装规则,安装的内容可以包括目标二进制、动态库、静态库以及文件、目录、脚本等。

INSTALL 指令包含了各种安装类型,我们需要一个个分开解释:

目标文件的安装:

1
2
3
4
5
6
7
8
9
INSTALL(TARGETS targets...
[[ARCHIVE|LIBRARY|RUNTIME]
[DESTINATION <dir>]
[PERMISSIONS permissions...]
[CONFIGURATIONS
[Debug|Release|...]]
[COMPONENT <component>]
[OPTIONAL]
] [...])

参数中的 TARGETS 后面跟的就是我们通过 ADD_EXECUTABLE 或者 ADD_LIBRARY 定义的目标文件,可能是可执行二进制、动态库、静态库。目标类型也就相对应的有三种,ARCHIVE 特指静态库,LIBRARY 特指动态库,RUNTIME 特指可执行目标二进制。

DESTINATION 定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这时候 CMAKE_INSTALL_PREFIX 其实就无效了。如果你希望使用 CMAKE_INSTALL_PREFIX 来定义安装路径,就要写成相对路径,即不要以/开头,那么安装后的路径就是${CMAKE_INSTALL_PREFIX}/<DESTINATION定义的路径>

举个简单的例子:

1
2
3
4
5
INSTALL(TARGETS myrun mylib mystaticlib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION libstatic
)

上面的例子会将:

可执行二进制 myrun 安装到 ${CMAKE_INSTALL_PREFIX}/bin 目录, 动态库 libmylib 安装到 ${CMAKE_INSTALL_PREFIX}/lib 目录,静态库 libmystaticlib 安装到 ${CMAKE_INSTALL_PREFIX}/libstatic 目录,特别注意的是:你不需要关心 TARGETS 具体生成的路径,只需要写上 TARGETS 名称就可以了。


打包

CPack是作为一个模块出现在cmake构建系统中的,它是一个非常强大的打包工具,可以用来打包二进制文件或者源码。打包好的二进制文件中包含了所有的cmake install命令需要的安装文件。在打包源码时,也可以生成对应的压缩包。 cpack可以依赖cmake构建生成的config文件,也可以自己编写配置文件

流程

  1. 编写好 CMakeLists.txt 打包指令. 然后 build, 会生成打包配置文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 打包 源码
    set(CPACK_SOURCE_GENERATOR "TGZ")
    set(CPACK_SOURCE_PACKAGE_FILE_NAME ${PROJECT_NAME}-${PROJECT_VERSION_FULL}-src) # 打包文件名称
    # 生成打包 源码 的配置: cmake-build-debug/CPackSourceConfig.cmake

    # 打包 可执行文件
    set(CPACK_GENERATOR "TGZ")
    set(CPACK_PACKAGE_FILE_NAME ${PROJECT_NAME}-${PROJECT_VERSION_FULL}-bin) # 打包文件名称
    # 生成打包 可执行文件 的配置: cmake-build-debug/CPackConfig.cmake
  2. 使用 cpack 指定配置文件进行打包

    1
    $ cpack --config CPackSourceConfig.cmake

    示例:

    1
    2
    3
    4
    5
    6
    7
    E:\ws_cpp\CMakeLab\cmake-build-debug (master -> origin)
    $ cpack --config CPackSourceConfig.cmake
    CPack: Create package using TGZ
    CPack: Install projects
    CPack: - Install directory: E:/ws_cpp/CMakeLab
    CPack: Create package
    CPack: - package: E:/ws_cpp/CMakeLab/pack/CMakeLab-1.3.1-src.tar.gz generated.

打包格式

set(CPACK_SOURCE_GENERATOR “TGZ”)

  1. 7Z-7Zzip-(.7z)
  2. TBZ2(tar.bz2)
  3. TGZ(.tar.gz)
  4. TXZ(.tar.xz)
  5. TZ(.tar.Z)
  6. ZIP(.zip)

踩坑

报错: undefined reference to `WinMain’

是编译 可执行文件 时, 找不到 main 入口, 可能两个原因:

  1. main 入口写错了, 正确的写法

    1
    2
    3
    int main(int argc, char *argv[]) {
    return 0;
    }
  2. 包含 main 入口的文件未加入到编译中, 应该

    1
    2
    3
    set(SOURCE_FILES main.cpp)
    message(STATUS "--- SOURCE_FILES: ${SOURCE_FILES}") # 查看所有文件
    add_executable(CMakeLab ${SOURCE_FILES})

报错: exit code -1073741515 (0xC0000135)

原因是找不到 动态库, 可以通过两种方式解决:

  1. 需要将 动态库 丢到与 TestCpp.exe 同一目录.
  2. 将 动态库 所在的目录加入环境变量.