cmake使用总结

Last Updated: 2023-12-03 04:00:27 Sunday

-- TOC --

cmake根据配置文件生成Makefile,最后还是make。学习cmake最重要的是实践,不断地实践...

out-of-source build

通过在项目目录中创建另一个build目录,并在build目录中执行cmake .. && make,这就是out-of-source build,这是cmake官方强烈推荐的做法。编译链接生成的所有文件都在build目录中,不会影响工程目录。(如果直接在工程目录中cmake .,这种做法叫in-source build)

示例如下:

$ cmake --version
$ cd path/to/project
$ mkdir build
$ cd build
$ cmake .. && make

目录名称为build只是个习惯,完全可以用别的名称,多次build可以使用多个不同的目录名,相互之间不影响!

cmake生成的Makefile,没有distclean这个目标(但有make clean),实现make distclean的方法,就是将用于out-of-source build的目录,直接删除即可。当工程存在多个SUBDIRECTORY的时候,out-of-source build几乎唯一的选择,否则,cmake生成的中间文件,也将分布在各个SUBDIRECTORY之中,这会让你很痛苦很麻烦。

常用make目标(与cmake无关)

$ make help
$ make clean
$ make VERBOSE=1

常用cmake指令

$ cmake --fresh ..  # after CMakeLists.txt has been modified

示例学习Case 1

CMAKE_MINIMUM_REQUIRED(VERSION 3.0)
PROJECT(u8_ring_buffer VERSION 0.37.1.2)
MESSAGE("## project: " ${PROJECT_NAME})
MESSAGE("## cmake project: " ${CMAKE_PROJECT_NAME})
MESSAGE("## binary dir: " ${PROJECT_BINARY_DIR})
MESSAGE("## source dir: " ${PROJECT_SOURCE_DIR})
MESSAGE("## project version: " ${PROJECT_VERSION})
MESSAGE("## cmake project version: " ${CMAKE_PROJECT_VERSION})
MESSAGE("## project version major: " ${PROJECT_VERSION_MAJOR})
MESSAGE("## project version minor: " ${PROJECT_VERSION_MINOR})
MESSAGE("## project version patch: " ${PROJECT_VERSION_PATCH})
MESSAGE("## project version tweak: " ${PROJECT_VERSION_TWEAK})

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
MESSAGE("## exec out dir: " ${EXECUTABLE_OUTPUT_PATH})

SET(TARGET "u8rb")
SET(SRCS utest.cpp u8_ring_buffer.cpp)
ADD_COMPILE_OPTIONS(-Wall -Wextra)
ADD_EXECUTABLE(${TARGET} ${SRCS})
TARGET_LINK_LIBRARIES(${TARGET} gtest)

一般CMakeLists.txt文件,第1行指令都是CMAKE_MINIMUM_REQUIRED(VERSION 3.0),指定一个所需cmake的最小版本。写此文的时候,系统中的cmake版本为3.25,因此这里选择了3.0,测试发现,3.0版本已经可以正常使用下面的VERSION指令。

PROJECT(u8_ring_buffer VERSION 0.37.1.2)被定义后,后面那些个macro就自动都有了。打印效果:

## project: u8_ring_buffer
## cmake project: u8_ring_buffer
## binary dir: /home/xinlin/test/cmake_test/ring_buffer/build
## source dir: /home/xinlin/test/cmake_test/ring_buffer
## project version: 0.37.1.2
## cmake project version: 0.37.1.2
## project version major: 0
## project version minor: 37
## project version patch: 1
## project version tweak: 2
## exec out dir: /home/xinlin/test/cmake_test/ring_buffer/build/bin

按照cmake对4数字组合版本号的翻译,依次为:major.minor.patch.tweak

PROJECT指令中还可以带上LANGUAGES,如PROJECT(u8_ring_buffer VERSION 0.37.1.2 LANGUAGES "CXX"),CXX表示C++,此时cmake会检查C++编译器是否存在,以便工程可以正确构建。如果不指定LANGUAGES,默认是C和C++

build type

$ cmake -DCMAKE_BUILD_TYPE=Debug ..
$ cmake -DCMAKE_BUILD_TYPE=Release ..

Typical values include Debug, Release, RelWithDebInfo and MinSizeRel, but custom build types can also be defined.

当使用-DCMAKE_BUILD_TYPE=Release时,通过make VERBOSE=1,可以看到自动添加的编译参数-O3 -DNDEBUG

[ 33%] Building CXX object CMakeFiles/u8rb.dir/utest.cpp.o
/usr/bin/c++   -O3 -DNDEBUG -MD -MT CMakeFiles/u8rb.dir/utest.cpp.o -MF CMakeFiles/u8rb.dir/utest.cpp.o.d -o CMakeFiles/u8rb.dir/utest.cpp.o -c /home/xinlin/test/cmake_test/ring_buffer/utest.cpp
[ 66%] Building CXX object CMakeFiles/u8rb.dir/u8_ring_buffer.cpp.o
/usr/bin/c++   -O3 -DNDEBUG -MD -MT CMakeFiles/u8rb.dir/u8_ring_buffer.cpp.o -MF CMakeFiles/u8rb.dir/u8_ring_buffer.cpp.o.d -o CMakeFiles/u8rb.dir/u8_ring_buffer.cpp.o -c /home/xinlin/test/cmake_test/ring_buffer/u8_ring_buffer.cpp
[100%] Linking CXX executable bin/u8rb
/usr/bin/cmake -E cmake_link_script CMakeFiles/u8rb.dir/link.txt --verbose=1
/usr/bin/c++ -O3 -DNDEBUG -rdynamic CMakeFiles/u8rb.dir/utest.cpp.o CMakeFiles/u8rb.dir/u8_ring_buffer.cpp.o -o bin/u8rb  -lgtest 

默认CMAKE_BUILD_TYPE未定义,就没有-O3 -DNDEBUG的效果。

一般不会在CMakeLists.txt中设置CMAKE_BUILD_TYPE,都是用户在命令行指定。但Windows系统的lib库没有固定的存放位置,而且debug和release需要分开,可能必须要指定CMAKE_BUILD_TYPE的值。

判断变量是否有存在

IF(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
    IF(NOT DEFINED CMAKE_BUILD_TYPE)
        SET(CMAKE_BUILD_TYPE "DEBUG")
    ENDIF()
ENDIF()

IF(NOT DEFINED CMAKE_BUILD_TYPE)

如果做存在判断:

IF(DEFINED CMAKE_BUILD_TYPE)

示例学习Case 2

CMAKE_MINIMUM_REQUIRED(VERSION 3.0)
PROJECT(ring_buffer_template)

SET(CMAKE_CXX_STANDARD 23)
SET(CMAKE_CXX_STANDARD_REQUIRED True)
#SET(CMAKE_CXX_EXTENSIONS OFF)
UNSET(CMAKE_CXX_STANDARD_REQUIRED)

INCLUDE_DIRECTORIES(include)

SET(TGT "rbt")
SET(SRCS utest.cpp)
ADD_EXECUTABLE(${TGT} ${SRCS})
TARGET_LINK_LIBRARIES(${TGT} gtest)

PROJECT_SOURCE_DIR下只有一个utest.cpp,而ring_buffer.hpp文件放在了${PROJECT_SOURCE_DIR}/include下面,此hpp文件使用了C++17的特性。在utest.cpp中,只是简单的#iniclude "ring_buffer.hpp",这行include没有路径信息。

$ tree
.
├── CMakeLists.txt
├── include
│   └── ring_buffer.hpp
└── utest.cpp

1 directory, 3 files

部分输出:

[ 50%] Building CXX object CMakeFiles/rbt.dir/utest.cpp.o
/usr/bin/c++  -I/home/xinlin/test/cmake_test/ring_buffer_template/include -std=gnu++23 -MD -MT CMakeFiles/rbt.dir/utest.cpp.o -MF CMakeFiles/rbt.dir/utest.cpp.o.d -o CMakeFiles/rbt.dir/utest.cpp.o -c /home/xinlin/test/cmake_test/ring_buffer_template/utest.cpp

示例学习Case 3

这个case有3个subdirectory,项目的目录树如下:

$ tree
.
├── CMakeLists.txt
├── common
│   ├── CMakeLists.txt
│   ├── common.cpp
│   └── common.h
├── src
│   ├── CMakeLists.txt
│   └── test_main.cpp
└── src2
    ├── CMakeLists.txt
    ├── show_func.cpp
    └── test_main2.cpp

下面是4个CMakeLists.txt的内容:

$ cat CMakeLists.txt
CMAKE_MINIMUM_REQUIRED(VERSION 3.0)
PROJECT(mytt)

# ADD_SUBDIRECTORY用于向工程添加子目录,
# 每个子目录都是个模块,有自己的target,或者executable,或者library。
# lib, src_bin, src2_bin分别是在编译这些子目录时的输出目录,如build/lib。
# 如果不填写输出目录,就直接使用与subdirectory同名的目录,如build/common。
ADD_SUBDIRECTORY(common lib)
ADD_SUBDIRECTORY(src src_bin)
ADD_SUBDIRECTORY(src2 src2_bin)
$ cat common/CMakeLists.txt
# AUX_SOURCE_DIRECTORY用于将当前(.)路径下的所有cpp文件存入变量SRC_COMMON。
AUX_SOURCE_DIRECTORY(. SRC_COMMON)
ADD_COMPILE_OPTIONS(-Wall -Wextra)
# common目录下的target是一个名为ttcm.a的静态库。
ADD_LIBRARY(ttcm STATIC ${SRC_COMMON})
# 子目录下也可以有多个target,但是默认名称不能相同。
ADD_LIBRARY(ttcms SHARED ${SRC_COMMON})
$ cat src/CMakeLists.txt
ADD_EXECUTABLE(tt test_main.cpp)
# 似乎用INCLUDE_DIRECTORIES指令更好,只是包含头文件,
# 不知道PRIVATE是什么含义?...
# 在subdirectory的CMakeLists.txt中,可直接使用主CMakeLists.txt中设定的变量。
TARGET_INCLUDE_DIRECTORIES(tt PRIVATE ${PROJECT_SOURCE_DIR}/common)
# ttcm.a在build/lib目录下,直接就被找到了,无需专门设置路径。
TARGET_LINK_LIBRARIES(tt ttcm)
$ cat src2/CMakeLists.txt
AUX_SOURCE_DIRECTORY(. SRC2)
# 下面这行的打印为:
# ## SRC2: ./show_func.cpp./test_main2.cpp
MESSAGE("## SRC2: " ${SRC2})
ADD_EXECUTABLE(tt2 ${SRC2})
# TARGET_COMPILE_OPTIONS放在ADD_EXECUTABLE是OK的,
# 但是依然不知道PRIVATE是啥意思?
# 也可以使用ADD_COMPILE_OPTIONS,但要放在ADD_EXECUTABLE之前。
TARGET_COMPILE_OPTIONS(tt2 PRIVATE -Wall -Wextra)
TARGET_INCLUDE_DIRECTORIES(tt2 PRIVATE ${PROJECT_SOURCE_DIR}/common)
TARGET_LINK_LIBRARIES(tt2 ttcm)

include_directories(header-dir)是一个全局包含,向下传递。就是说如果某个目录的CMakeLists.txt中使用了该指令,其下所有的子目录默认也包含了header-dir目录。当然了,在最终子目录的 CMakeLists.txt 文件中,使用 include_directories() 和 target_include_directories() 的效果是相同的。

将common编译成静态库的目的,是为了在后面编译src和src2的时候,不要重复编译common内的代码。

这个case有好几个target,这些targe都在cmake生成的Makefile中:

$ make help
The following are some of the valid targets for this Makefile:
... all (the default if no target is provided)
... clean
... depend
... edit_cache
... rebuild_cache
... tt
... tt2
... ttcm
... ttcms

ADD_DEFINITIONS

就在在编译器命令行,增加-D参数,做条件编译!在支持多OS的时候,这个cmake指令会非常方便。

Adds -D define flags to the compilation of source files. Adds definitions to the compiler command line for sources in the current directory and below. This command can be used to add any flags, but it is intended to add preprocessor definitions.

比如:

ADD_DEFINITIONS(-DOS_WINDOWS)

用make VERBOSE=1时,能看到:

[ 50%] Building CXX object CMakeFiles/cppone.dir/cppone.cpp.o
/usr/bin/c++ -DOS_WINDOWS  -Wall -Wextra -std=gnu++11 -MD -MT CMakeFiles/cppone.dir/cppone.cpp.o -MF CMakeFiles/cppone.dir/cppone.cpp.o.d -o CMakeFiles/cppone.dir/cppone.cpp.o -c /home/xinlin/test/cmake_test/cppone/cppone.cpp

测试发现,在cmake命令行使用-D,必须采用type=value的形式,这些定义,相当于在cmake脚本中使用SET指令。注意区分!

判断OS和编译器

不同的OS,不同编译器,有一些cmake指令是不同的。

用这条指令可以查看OS SYSTEM NAME和COMPILER ID,CMAKE_SYSTEM_NAMECMAKE_CXX_COMPILER_ID是cmake自动生成的。

MESSAGE("## OS SYSTEM: "${CMAKE_SYSTEM_NAME})
MESSAGE("## COMPILER ID: "${CMAKE_CXX_COMPILER_ID})

SYSTEM NAME有LinuxWindowsFreeBSD等等。

COMPILER ID有MSVC,gcc是GNU,还有Clang,更多请参考:https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_COMPILER_ID.html

此时,基本都会用到cmake的IF语句:https://cmake.org/cmake/help/latest/command/if.html

IF(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU")
    ADD_COMPILE_OPTIONS(-Wall -Wextra)
ELSEIF(${CMAKE_CXX_COMPILER_ID} STREQUAL "MSVC")
    MESSAGE("## MSVC")
ELSE()
    MESSAGE("##")
ENDIF()

指定编译器

$ cmake -DCMAKE_CXX_COMPILER=clang++ ..

这个指令与INCLUDE_DIRECTORIES很相似,一个给出头文件路径,一个给出库文件的路径。如果不指定,Linux下是有默认路径的,而Windows上比较麻烦,所以一般都要设置LINK_DIRECTORIES。

QT项目

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

AUTOMOC是一个布尔值,指定 CMake 是否会自动处理 Qt moc 预处理器,即不必使用 QT4_WRAP_CPP() 、 QT5_WRAP_CPP() 等命令。目前,支持 Qt 版本 4 到 6。如果在创建目标时设置了此属性,则通过 CMAKE_AUTOMOC 变量的值来初始化此属性。当此属性设置为 ON 时,CMake将在构建时扫描头文件和源文件,并相应地调用 moc 。

RCC和UIC,与MOC一样,都是QT特有的一些代码预处理。

本文链接:https://cs.pynote.net/sf/c/cdm/202301021/

-- EOF --

-- MORE --