前言

CMake 是非常常见的 C++ 代码构建工具,在 C++ 项目构建中被广泛使用。

本文我们将以著名的 CMake 入门示例 Useful CMake Examples - Github 为例,进行 CMake 的使用汇总。


快速开始

链接示例

这个示例中包含 CMake 配置文件的基本结构,同时包含头文件、静态库、动态库的链接方式。

最后,我们还在这个示例中包含了安装的示例,尽管这并不常用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 定义项目名和最小支持版本
cmake_minimum_required(VERSION 3.5)
project(hello_binary)

############################################################
# 配置
############################################################

# 内置变量配置
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/) # 动态库保存位置
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/) # 静态库保存位置
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -D EX2) # 全局额外编译选项

# 自定义变量
set(SRC
${PROJECT_SOURCE_DIR}/src/main.cpp # 源文件
)
set(LIBRARY
${PROJECT_SOURCE_DIR}/src/Hello.cpp # 库文件
)
set(INCLUDE
${PROJECT_SOURCE_DIR}/include/ # 头文件
)

############################################################
# 构建
############################################################

# 创建可执行目标
add_executable(main ${SRC}) # 可执行文件 main, 包含源文件 ${SRC}
target_link_libraries(main
PRIVATE
shared_library # 添加静态库依赖
)
# target_include_directories(main
# PRIVATE
# ${INCLUDE} # 由于 shared_library 的 include 是 PUBLIC 的, 因此可以传递
# )

# 创建库目标
add_library(shared_library SHARED ${LIBRARY}) # 动态 (SHARED) 库 shared_library, 包含源文件 ${LIBRARY}
target_include_directories(shared_library
PUBLIC
${INCLUDE} # 添加头文件依赖 ${INCLUDE}
)

############################################################
# 安装 (一般不需要)
############################################################

install(
TARGETS main
DESTINATION bin/
)
install(
TARGETS shared_library
DESTINATION lib/
)
install(
FILES ${PROJECT_SOURCE_DIR}/example.conf
DESTINATION etc/
)
install(
DIRECTORY ${PROJECT_SOURCE_DIR}/include/
DESTINATION include/
)

1
2
3
4
5
6
mkdir -p build     # 创建构建目录
cd build # 切换到构建目录

cmake .. # 初始化 cmake
make # 构建主程序
sudo make install # 安装

设置变量

配置内置变量

CMake 中可以说使用 set 设置变量,其中一些变量是内置的,用户可以通过修改这些值用于配置。

1
2
3
4
set(CMAKE_BUILD_TYPE Debug)  # 默认值: Release, 可选值: Debug / Release / MinSizeRel
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/) # 默认值: ${CMAKE_BINARY_DIR}
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/) # 默认值: ${CMAKE_BINARY_DIR}
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D NDEBUG -std=c++20") # 默认值: 无
内置变量 含义 默认值(候选值)
CMAKE_SOURCE_DIR 源文件根目录 启动 CMakeLists.txt 目录
CMAKE_CURRENT_SOURCE_DIR 当前源文件目录 当前 CMakeLists.txt 目录
CMAKE_BINARY_DIR 二进制文件根目录 cmake 命令工作目录
CMAKE_CUPPENT_BINARY_DIR 当前二进制文件目录 cmake 命令工作目录下的 source 同步目录
PROJECT_NAME 项目名称 project(...) 定义
PROJECT_SOURCE_DIR 项目源文件目录 CMAKE_CURRENT_SOURCE_DIR
PROJECT_BINARY_DIR 项目二进制文件目录 CMAKE_CURRENT_BINARY_DIR
CMAKE_RUNTIME_OUTPUT_DIRECTORY 可执行文件保存目录 PROJECT_BINARY_DIR
CMAKE_LIBRARY_OUTPUT_DIRECTORY 动态库保存目录 PROJECT_BINARY_DIR
CMAKE_ARCHIVE_OUTPUT_DIRECTORY 静态库保存目录 PROJECT_BINARY_DIR
CMAKE_BUILD_TYPE 自定义构建方式 不确定(Debug / Release / etc.)
CMAKE_C_COMPILER C 编译器路径 不确定(cc / gcc / clang / etc.)
CMAKE_CXX_COMPILER C++ 编译器路径 不确定(c++ / g++ / clang++ / etc.)
CMAKE_CXX_FLAGS 全局额外编译标志 无(-g / -O2 / -DNDEBUG / etc.)
CMAKE_EXPORT_COMPILE_COMMANDS 输出 compile_commands.json OFFON

使用命令变量

不仅可以在 CMakeLists.txt 中设置内置变量,我们也可以使用启动命令设置它:

1
2
cmake .. -D CMAKE_BUILD_TYPE=Debug -D CMAKE_CXX_COMPILER=clang++-14
# 如果 CMakeLists.txt 设置将覆盖外部设置

自定义变量

除配置变量外,CMake 还可以自定义变量,它们通常用于保存一些路径,例如:

1
2
set(SRC main.cpp add.cpp sub.cpp mul.cpp div.cpp)  # 设置 SRC 变量
add_executable(main ${SRC}) # 使用 SRC 变量

CMake 构建选项

自定义构建方式

CMake 的默认构建方式通常是 ReleaseDebug,这在不同发行版中不同。

可以使用 -D CMAKE_BUILD_TYPE 自定义构建方式:

1
cmake .. -D CMAKE_BUILD_TYPE=Debug

CMAKE_BUILD_TYPE 有四个保有取值:

CMAKE_BUILD_TYPE 描述
Debug 调试版本,包含大量调试信息。
Release 发行版本,使用尽可能多的编译器优化。
RelWithDebInfo 发行版本,但包含必要调试信息。
MinSizeRel 发行版本,但最小化引用程序大小。

当然,你也可以使用 CMakeLists.txt 中的 set 修改该内置变量并且它的优先级高于命令参数,但我们通常不这么做。

自定义构建编译器

CMake 的默认构建编译器通常是 c++ / g++ / cl 等,在不同平台或发行版中也会有所不同。

可以使用 -D CMAKE_CXX_COMPILER 自定义构建编译器路径:

1
cmake .. -D CMAKE_C_COMPILER=clang-14 -D CMAKE_CXX_COMPILER=clang++-14

clang 是一个在项目开发中备受欢迎的编译器版本。

自定义构建工具

CMake 的自动构建工具通常是 make,这是一种古老的项目构建工具,它至今依然应用广泛。但在一些项目中有时更青睐于使用其他构建工具,ninja 就是其中一种。

使用 Makefile 构建:

1
2
cmake .. -G "Unix Makefiles"  # 使用 Makefile 构建
make

使用 Ninja 构建:

1
2
cmake .. -G Ninja             # 使用 Ninja 构建
ninja

目标属性

目标、属性、依赖是 cmake 的三大组成单元。我们可以为目标添加属性,同时目标之间形成依赖关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 创建 main 可执行目标
add_executable(main ${SRC})

# 添加宏定义
# 等价于 g++ ... -D NDEBUG
target_compile_definitions(main
PRIVATE
NDEBUG
)

# 添加头目录
# 等价于 g++ ... -I $PROJECT_SOURCE_DIR/include/
target_include_directories(main
PRIVATE
${PROJECT_SOURCE_DIR}/include/
)

# 添加库目录
# 等价于 g++ ... -L $CMAKE_BINARY_DIR/lib/
target_link_directories(main
PRIVATE
${CMAKE_BINARY_DIR}/lib/
)

# 添加库文件
# 等价于 g++ ... -l lib1 -l lib2
target_link_libraries(main
PRIVATE
lib1
lib2
)

# 添加预编译头文件
# 等价于 g++ ... -include header1 -include header2
target_precompile_headers(main
PRIVATE
header1
header2
)

# 自定义编译选项
# 相当于直接附加其他编译选项, 最自由的方式
target_compile_options(main
PRIVATE
-std=c++20
)

多项目链接

链接第三方库

如果你已将第三方库安装到系统目录,可以使用下面的方法获取相应的库并完成链接。

你可以通过 find_package 查找系统路径下的第三方库,同时 CMake 将为你创建一些变量:

  • xxx_FOUNDTRUE / FALSE。是否找到该第三方库。
  • xxx_VERSION:第三方库版本号。
  • xxx_INCLUDE_DIRS:第三方库包含的头目录。
  • xxx_LIBRARIES:第三方库包含的库文件。
  • xxx_DEFINITIONS:第三方库定义的变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cmake_minimum_required(VERSION 3.5)
project(third-party-library)

# 请先安装 Boost 库
# sudo apt install libboost-all-dev
# 在系统目录查找 Boost::filesystem
find_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem)
message("Boost_FOUND: ${Boost_FOUND}") # Boost_FOUND: TRUE
message("Boost_VERSION: ${Boost_VERSION}") # Boost_VERSION: 1.74.0
message("Boost_INCLUDE_DIRS: ${Boost_INCLUDE_DIRS}") # Boost_INCLUDE_DIRS: /usr/include
message("Boost_LIBRARIES: ${Boost_LIBRARIES}") # Boost_LIBRARIES: Boost::filesystem
if (${Boost_DEFINITIONS}) # Boost_DEFINITIONS: None
message("Boost_DEFINITIONS: ${Boost_DEFINITIONS}")
else()
message("Boost_DEFINITIONS: None")
endif()

add_executable(main main.cpp)
target_link_libraries(main
PRIVATE
Boost::filesystem # 链接 Boost::filesystem, 无需 include 因为它是 PUBLIC 子属性
)

链接子项目

项目文件树:

文件依赖关系:

CMake 链接方式:

1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.5)
project(sub-projects-basic)

# 设置层次化标记: 命名空间
set(NAMESPACE "ROOT")
message(${NAMESPACE})

# 构建三个子项目
add_subdirectory(subbinary)
add_subdirectory(sublibrary1)
add_subdirectory(sublibrary2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
project(main)

# 内层变量将覆盖外层变量, 类似于命名空间的概念
set(NAMESPACE ${NAMESPACE}::${PROJECT_NAME})
message(${NAMESPACE})

set(SRC ${PROJECT_SOURCE_DIR}/main.cpp)

# 新增可执行目标
add_executable(${PROJECT_NAME} ${SRC})
# 重命名可执行目标为 ROOT::main::main
add_executable(${NAMESPACE}::main ALIAS ${PROJECT_NAME})
target_link_libraries(${PROJECT_NAME}
PRIVATE
ROOT::sublib1::lib
ROOT::sublib2::lib
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
project(sublib1)

# 内层变量将覆盖外层变量, 类似于命名空间的概念
set(NAMESPACE ${NAMESPACE}::${PROJECT_NAME})
message(${NAMESPACE})

set(LIB ${PROJECT_SOURCE_DIR}/src/sublib1.cpp)
set(INC ${PROJECT_SOURCE_DIR}/include/)

# 新增对象库目标
# 对象库目标不实际构建库文件, 用于在多目标间共享, 可以像静态库一样使用.
add_library(${PROJECT_NAME} OBJECT ${LIB})
# 重命名对象目标
add_library(${NAMESPACE}::lib ALIAS ${PROJECT_NAME})
target_include_directories(${PROJECT_NAME} PUBLIC ${INC})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
project(sublib2)

# 内层变量将覆盖外层变量, 类似于命名空间的概念
set(NAMESPACE ${NAMESPACE}::${PROJECT_NAME})
message(${NAMESPACE})

set(INC ${PROJECT_SOURCE_DIR}/include/)

# 新增接口库目标
# 接口库目标无需源文件, 用于在多目标间共享配置, 所有接口库的属性都必须是 INTERFACE 的
add_library(${PROJECT_NAME} INTERFACE)
add_library(${NAMESPACE}::lib ALIAS ${PROJECT_NAME})
target_include_directories(${PROJECT_NAME} INTERFACE ${INC})


代码生成

CMake 提供了 configure_file 进行代码生成,它可以将原代码中的 ${xxx}@xxx@ 进行替换。

path.hpp.in

1
2
3
4
5
6
7
8
9
10
#ifndef __PATH_H__
#define __PATH_H__

// version variable that will be substituted by cmake
// This shows an example using the @ variable type
const char* ver = "${cf_example_VERSION}";
const char* path = "@CMAKE_SOURCE_DIR@";

#endif

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(configure-file)

configure_file(path.hpp.in ${PROJECT_BINARY_DIR}/path.hpp)
# 既替换 @xxx@, 又替换 ${xxx}

add_executable(main ${PROJECT_SOURCE_DIR}/main.cpp)
target_include_directories(main PRIVATE ${PROJECT_BINARY_DIR}/)

path.hpp

1
2
3
4
5
6
7
8
9
10
#ifndef __PATH_H__
#define __PATH_H__

// version variable that will be substituted by cmake
// This shows an example using the @ variable type
const char* ver = "0.2.1";
const char* path = "~/code/cmake-examples/03-code-generation/configure-files";

#endif

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(configure-file)

configure_file(path.hpp.in ${PROJECT_BINARY_DIR}/path.hpp @ONLY)
# 只替换 @xxx@, 不替换 ${xxx}

add_executable(main ${PROJECT_SOURCE_DIR}/main.cpp)
target_include_directories(main PRIVATE ${PROJECT_BINARY_DIR}/)

path.hpp

1
2
3
4
5
6
7
8
9
10
#ifndef __PATH_H__
#define __PATH_H__

// version variable that will be substituted by cmake
// This shows an example using the @ variable type
const char* ver = "${cf_example_VERSION}";
const char* path = "~/code/cmake-examples/03-code-generation/configure-files";

#endif


静态分析

ClangFormat 代码格式化

ClangFormat 基本使用

Clang Format 是非常常用的代码格式化工具,个人以为其命令行使用比 CMake 链接重要。

请先确认正常安装 clang-format,它通常跟随版本号,如我使用的是 clang-format-14,这也将作为我后面的示例。

1
2
3
4
5
6
7
8
9
# clang-format 的常用操作
clang-format-14 {filename} # 使用默认配置格式化, 输出到终端
clang-format-14 {filename} -style=file -i # 原地格式化, 使用 .clang-format 文件配置
clang-format-14 {filename} --style={LLVM|GNU|Google|Chromium|Microsoft|Mozilla|WebKit} -i
# 原地格式化, 使用 LLVM / GNU / Google / Chromium / Microsoft / Mozilla / WebKit 的默认配置
clang-format-14 --style={{LLVM|GNU|Google|Chromium|Microsoft|Mozilla|WebKit}} --dump-config
# 查看默认配置选项的详细默认配置, 输出到终端
clang-format-14 {filename} -output-replacements-xml
# 输出详细格式化差异, 以 XML 格式输出到终端, 通常用于与工具链接

官方文档:ClangFormat Documentation

配置手册:Clang-Format Style Options Documentation

CMake 构建目标

项目文件树:

CMake 链接方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# CMakeLists.txt
cmake_minimum_required (VERSION 3.5)
project(clang_format)

# 添加 CMake 文件模块目录
set(CMAKE_MODULE_PATH
${CMAKE_MODULE_PATH}
${CMAKE_SOURCE_DIR}/cmake/modules
)
message("CMAKE_MODULE_PATH: ${CMAKE_MODULE_PATH}")

# 链接子项目
add_subdirectory(subproject1)
add_subdirectory(subproject2)

# 配置和构建伪目标
set(CLANG_FORMAT_BIN clang-format-14)
set(CLANG_FORMAT_EXCLUDE_PATTERNS "build/" ${CMAKE_BINARY_DIR})
include(clang-format) # cmake/modules/clang-format.cmake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# cmake/modules/clang-format.cmake
# A CMake script to find all source files and setup clang-format targets for them

############################################################
# 搜索源文件
############################################################
set(CLANG_FORMAT_CXX_FILE_EXTENSIONS ${CLANG_FORMAT_CXX_FILE_EXTENSIONS} *.cpp *.h *.cxx *.hxx *.hpp *.cc *.ipp)
file(GLOB_RECURSE ALL_SOURCE_FILES ${CLANG_FORMAT_CXX_FILE_EXTENSIONS})

# 设置黑名单
set(CLANG_FORMAT_EXCLUDE_PATTERNS ${CLANG_FORMAT_EXCLUDE_PATTERNS} "/CMakeFiles/" "/cmake/")

# 剔除黑名单
foreach (SOURCE_FILE ${ALL_SOURCE_FILES})
foreach (EXCLUDE_PATTERN ${CLANG_FORMAT_EXCLUDE_PATTERNS})
string(FIND ${SOURCE_FILE} ${EXCLUDE_PATTERN} EXCLUDE_FOUND)
if (NOT ${EXCLUDE_FOUND} EQUAL -1)
list(REMOVE_ITEM ALL_SOURCE_FILES ${SOURCE_FILE})
endif ()
endforeach ()
endforeach ()

message("ALL_SOURCE_FILES: ${ALL_SOURCE_FILES}")

############################################################
# 伪目标 format
############################################################

add_custom_target(format
COMMENT "Running clang-format to change files"
COMMAND ${CLANG_FORMAT_BIN}
-style=file
-i
${ALL_SOURCE_FILES}
)

############################################################
# 伪目标 format-check
############################################################

add_custom_target(format-check
COMMENT "Checking clang-format changes"
# Use ! to negate the result for correct output
COMMAND !
${CLANG_FORMAT_BIN}
-style=file
-output-replacements-xml
${ALL_SOURCE_FILES}
| grep -q "replacement offset"
)

############################################################
# 伪目标 format-check-changed
############################################################

# Get the path to this file
get_filename_component(_clangcheckpath ${CMAKE_CURRENT_LIST_FILE} PATH)
# have at least one here by default
set(CHANGED_FILE_EXTENSIONS ".cpp")
foreach(EXTENSION ${CLANG_FORMAT_CXX_FILE_EXTENSIONS})
set(CHANGED_FILE_EXTENSIONS "${CHANGED_FILE_EXTENSIONS},${EXTENSION}" )
endforeach()

set(EXCLUDE_PATTERN_ARGS)
foreach(EXCLUDE_PATTERN ${CLANG_FORMAT_EXCLUDE_PATTERNS})
list(APPEND EXCLUDE_PATTERN_ARGS "--exclude=${EXCLUDE_PATTERN}")
endforeach()

# call the script to check changed files in git
add_custom_target(format-check-changed
COMMENT "Checking changed files in git"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${_clangcheckpath}/../scripts/clang-format-check-changed.py
--file-extensions \"${CHANGED_FILE_EXTENSIONS}\"
${EXCLUDE_PATTERN_ARGS}
--clang-format-bin ${CLANG_FORMAT_BIN}
)

与 CMake 无关的 Python 脚本,感兴趣的小伙伴可以学习一下。

源代码有 BUG 这里已经修改。个人感觉这份代码还有很多可以调整的逻辑,但是懒得修了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env python3

import argparse
import os
import sys
import subprocess


def check_file(filename, excludes, extensions):
"""
Check if a file should be included in our check
"""
name, ext = os.path.splitext(filename)

if len(ext) > 0 and ext in extensions:
if len(excludes) == 0:
return True

for exclude in excludes:
if exclude in filename:
return False

return True

return False


def check_directory(directory, excludes, extensions):
output = []

if len(excludes) > 0:
for exclude in excludes:
if exclude in directory:
directory_excluded = False
return output

for root, _, files in os.walk(directory):
for file in files:
filename = os.path.join(root, file)
if check_file(filename, excludes, extensions):
print("Will check file [{}]".format(filename))
output.append(filename)
return output


def get_git_root(git_bin):
cmd = [git_bin, "rev-parse", "--show-toplevel"]
try:
return subprocess.check_output(cmd).strip().decode()
except subprocess.CalledProcessError as e:
print("Error calling git [{}]".format(e))
raise


def clean_git_filename(line):
"""
Takes a line from git status --porcelain and returns the filename
"""
file = None
git_status = line[:2]
# Not an exhaustive list of git status output but should
# be enough for this case
# check if this is a delete
if 'D' in git_status:
return None
# ignored file
if '!' in git_status:
return None
# Covers renamed files
if '->' in line:
file = line[3:].split('->')[-1].strip()
else:
file = line[3:].strip()

return file


def get_changed_files(git_bin, excludes, file_extensions):
"""
Run git status and return the list of changed files
"""
extensions = file_extensions.split(",")
# arguments coming from cmake will be *.xx. We want to remove the *
for i, extension in enumerate(extensions):
if extension[0] == '*':
extensions[i] = extension[1:]

git_root = get_git_root(git_bin)

cmd = [git_bin, "status", "--porcelain", "--ignore-submodules"]
print("git cmd = {}".format(cmd))
output = []
returncode = 0
try:
cmd_output = subprocess.check_output(cmd).decode()
for line in cmd_output.split('\n'):
if len(line) > 0:
file = clean_git_filename(line)
if not file:
continue
file = os.path.join(git_root, file)

if file[-1] == "/":
directory_files = check_directory(
file, excludes, file_extensions)
output = output + directory_files
else:
if check_file(file, excludes, file_extensions):
print("Will check file [{}]".format(file))
output.append(file)

except subprocess.CalledProcessError as e:
print("Error calling git [{}]".format(e))
returncode = e.returncode

return output, returncode


def run_clang_format(clang_format_bin, changed_files):
"""
Run clang format on a list of files
@return 0 if formatted correctly.
"""
if len(changed_files) == 0:
return 0
cmd = [clang_format_bin, "-style=file",
"-output-replacements-xml"] + changed_files
# print("clang-format cmd = {}".format(cmd))
try:
cmd_output = subprocess.check_output(cmd).decode()
if "replacement offset" in cmd_output:
print("ERROR: Changed files don't match format")
return 1
except subprocess.CalledProcessError as e:
print("Error calling clang-format [{}]".format(e))
return e.returncode

return 0


def cli():
# global params
parser = argparse.ArgumentParser(prog='clang-format-check-changed',
description='Checks if files chagned in git match the .clang-format specification')
parser.add_argument("--file-extensions", type=str,
default=".cpp,.h,.cxx,.hxx,.hpp,.cc,.ipp",
help="Comma separated list of file extensions to check")
parser.add_argument('--exclude', action='append', default=[],
help='Will not match the files / directories with these in the name')
parser.add_argument('--clang-format-bin', type=str, default="clang-format",
help="The clang format binary")
parser.add_argument('--git-bin', type=str, default="git",
help="The git binary")
args = parser.parse_args()

# Run gcovr to get the .gcda files form .gcno
changed_files, returncode = get_changed_files(
args.git_bin, args.exclude, args.file_extensions)
if returncode != 0:
return returncode

return run_clang_format(args.clang_format_bin, changed_files)


if __name__ == '__main__':
sys.exit(cli())

运行 ClangFormat

1
2
3
4
5
6
7
8
9
10
11
# 创建 build 目录
mkdir -p build
cd build

# 构建主程序
cmake ..
make # 构建伪目标与是否构建程序无关

make format # 原地格式化
make format-check # 检查格式化, 存在未格式化将报错
make format-check-changed # 检查 uncommitted 的格式化, 存在未格式化将报错

CppCheck

官方文档:Cppcheck - A tool for static C/C++ code analysis

所谓静态分析就是无需编译和运行程序,就可以发现一些错误,它可以帮助开发者更早的发现和修复问题。

静态分析器本身就是一个编译器,但它更擅长发掘一些更深的隐藏问题,让开发者在运行前就能发现一些问题。

CMake 构建目标

项目文件树:

CMake 构建方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cmake_minimum_required (VERSION 3.5)
project(cppcheck_analysis)

# 请先安装 CppCheck 库
# sudo apt install cppcheck

# Have cmake create a compile database
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Add a custom CMake Modules directory
set(CMAKE_MODULE_PATH
${CMAKE_MODULE_PATH}
${CMAKE_SOURCE_DIR}/cmake/modules
)

# find the cppcheck binary
find_package(CppCheck)

# Add sub directories
add_subdirectory(subproject1)
add_subdirectory(subproject2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# Locate cppcheck
#
# This module defines
# CPPCHECK_BIN, where to find cppcheck
#
# To help find the binary you can set CPPCHECK_ROOT_DIR to search a custom path
# Exported argumets include
# CPPCHECK_FOUND, if false, do not try to link to cppcheck --- if (CPPCHECK_FOUND)
#
# CPPCHECK_THREADS_ARG - Number of threads to use (e.g. -j 3)
# CPPCHECK_PROJECT_ARG - The project to use (compile_comands.json)
# CPPCHECK_BUILD_DIR_ARG - The build output directory
# CPPCHECK_ERROR_EXITCODE_ARG - The exit code if an error is found
# CPPCHECK_SUPPRESSIONS - A suppressiosn file to use
# CPPCHECK_CHECKS_ARGS - The checks to run
# CPPCHECK_OTHER_ARGS - Any other arguments
# CPPCHECK_COMMAND - The full command to run the default cppcheck configuration
# CPPCHECK_EXCLUDES - A list of files or folders to exclude from the scan. Must be the full path
#
# if CPPCHECK_XML_OUTPUT is set before calling this. CppCheck will create an xml file with that name
# find the cppcheck binary

# if custom path check there first
if(CPPCHECK_ROOT_DIR)
find_program(CPPCHECK_BIN
NAMES
cppcheck
PATHS
"${CPPCHECK_ROOT_DIR}"
NO_DEFAULT_PATH)
endif()

find_program(CPPCHECK_BIN NAMES cppcheck)

if(CPPCHECK_BIN)
execute_process(COMMAND ${CPPCHECK_BIN} --version
OUTPUT_VARIABLE CPPCHECK_VERSION
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)

set(CPPCHECK_THREADS_ARG "-j4" CACHE STRING "The number of threads to use")
set(CPPCHECK_PROJECT_ARG "--project=${PROJECT_BINARY_DIR}/compile_commands.json")
set(CPPCHECK_BUILD_DIR_ARG "--cppcheck-build-dir=${PROJECT_BINARY_DIR}/analysis/cppcheck" CACHE STRING "The build directory to use")
# Don't show these errors
if(EXISTS "${CMAKE_SOURCE_DIR}/.cppcheck_suppressions")
set(CPPCHECK_SUPPRESSIONS "--suppressions-list=${CMAKE_SOURCE_DIR}/.cppcheck_suppressions" CACHE STRING "The suppressions file to use")
else()
set(CPPCHECK_SUPPRESSIONS "" CACHE STRING "The suppressions file to use")
endif()

# Show these errors but don't fail the build
# These are mainly going to be from the "warning" category that is enabled by default later
if(EXISTS "${CMAKE_SOURCE_DIR}/.cppcheck_exitcode_suppressions")
set(CPPCHECK_EXITCODE_SUPPRESSIONS "--exitcode-suppressions=${CMAKE_SOURCE_DIR}/.cppcheck_exitcode_suppressions" CACHE STRING "The exitcode suppressions file to use")
else()
set(CPPCHECK_EXITCODE_SUPPRESSIONS "" CACHE STRING "The exitcode suppressions file to use")
endif()

set(CPPCHECK_ERROR_EXITCODE_ARG "--error-exitcode=1" CACHE STRING "The exitcode to use if an error is found")
set(CPPCHECK_CHECKS_ARGS "--enable=warning" CACHE STRING "Arguments for the checks to run")
set(CPPCHECK_OTHER_ARGS "" CACHE STRING "Other arguments")
set(_CPPCHECK_EXCLUDES)

foreach(ex ${CPPCHECK_EXCLUDES})
list(APPEND _CPPCHECK_EXCLUDES "-i${ex}")
endforeach(ex)

set(CPPCHECK_ALL_ARGS
${CPPCHECK_THREADS_ARG}
${CPPCHECK_PROJECT_ARG}
${CPPCHECK_BUILD_DIR_ARG}
${CPPCHECK_ERROR_EXITCODE_ARG}
${CPPCHECK_SUPPRESSIONS}
${CPPCHECK_EXITCODE_SUPPRESSIONS}
${CPPCHECK_CHECKS_ARGS}
${CPPCHECK_OTHER_ARGS}
${_CPPCHECK_EXCLUDES}
)

if(NOT CPPCHECK_XML_OUTPUT)
set(CPPCHECK_COMMAND
${CPPCHECK_BIN}
${CPPCHECK_ALL_ARGS}
)
else()
set(CPPCHECK_COMMAND
${CPPCHECK_BIN}
${CPPCHECK_ALL_ARGS}
--xml
--xml-version=2
2> ${CPPCHECK_XML_OUTPUT})
endif()

endif()


# handle the QUIETLY and REQUIRED arguments and set YAMLCPP_FOUND to TRUE if all listed variables are TRUE
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(
CPPCHECK
DEFAULT_MSG
CPPCHECK_BIN)

mark_as_advanced(
CPPCHECK_BIN
CPPCHECK_THREADS_ARG
CPPCHECK_PROJECT_ARG
CPPCHECK_BUILD_DIR_ARG
CPPCHECK_ERROR_EXITCODE_ARG
CPPCHECK_SUPPRESSIONS
CPPCHECK_CHECKS_ARGS
CPPCHECK_OTHER_ARGS)

if(CPPCHECK_FOUND)
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/analysis/cppcheck)
add_custom_target(cppcheck-analysis
COMMAND ${CPPCHECK_COMMAND})
message("cppcheck found. Use cppccheck-analysis targets to run it")
else()
message("cppcheck not found. No cppccheck-analysis targets")
endif()

运行 CppCheck Analysis

1
2
3
4
5
6
7
8
9
# 创建 build 目录
mkdir -p build
cd build

# 构建主程序
cmake ..
make # 构建伪目标与是否构建程序无关

make cppcheck-analysis # 静态分析

单元测试

Boost 测试框架

CMake 构建单元测试

这里我们假设你已经书写了 unit_tests.cpp Boost 测试代码,我们会在后面介绍如何书写 Boost 测试代码。

项目文件树:

文件依赖关系:

CMake 链接方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(unit-testing-boost)

############################################################
# 构建
############################################################

# 构建测试单元一
# 这个项目比较简单, 实际直接构建测试程序即可, 但较复杂场景建议分离
add_library(Palindrome STATIC Palindrome.cpp)
target_include_directories(Palindrome
PUBLIC
${PROJECT_SOURCE_DIR}
)

# 构建测试单元二
# 这个项目比较简单, 实际直接构建测试程序即可, 但较复杂场景建议分离
add_library(Reverse STATIC Reverse.cpp)
target_include_directories(Reverse
PUBLIC
${PROJECT_SOURCE_DIR}
)

############################################################
# 测试
############################################################

# 确保安装 Boost 库
# sudo apt install libboost-all-dev
find_package(Boost REQUIRED COMPONENTS unit_test_framework)

# 构建测试程序
add_executable(unit_tests unit_tests.cpp)
target_include_directories(unit_tests
PRIVATE
${PROJECT_SOURCE_DIR}
)
target_link_libraries(unit_tests
PRIVATE
Palindrome # 链接测试单元一
Reverse # 链接测试单元二
Boost::unit_test_framework # 链接 Boost 测试框架
)

# 将测试注册到 CTest
enable_testing()
add_test(test_all unit_tests)

运行单元测试与调试

凡是 cmake 构建的测试程序调试方式其实都一样。

1
2
3
4
5
6
7
8
9
10
11
12
# 创建 build 目录
mkdir -p build
cd build

# 构建可调试测试程序
cmake .. -D CMAKE_BUILD_TYPE=Debug
make

make test # 使用 CTest 测试, 确保测试已注册
./unit_tests # 直接运行测试
gdb unit_tests # 使用 gdb / lldb / etc. 调试
# ... # 或者使用 IDE 测试 (推荐, 自己配置或者使用 CMake 插件)

书写 Boost 单元测试

需要注意的是单元测试框架都不能使用 main 函数,因为框架本质就是为用户提供支持测试的 main 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define BOOST_TEST_MODULE VsidCommonTest  // 测试模块名
#include <boost/test/unit_test.hpp>

// 测试套件: reverse_tests
BOOST_AUTO_TEST_SUITE(reverse_tests)

// 测试用例: reverse_tests.simple
BOOST_AUTO_TEST_CASE(simple) {
// ...
BOOST_ASSERT(ret == "olleH"); // 断言: 如果执行错误, 不再执行下一语句
// ...
}

// 另一测试用例: reverse_tests.empty
BOOST_AUTO_TEST_CASE(empty) {
// ...
BOOST_CHECK(ret == ""); // 检查: 即使执行错误, 依然执行下一语句
// ...
}

// 结束测试套件
BOOST_AUTO_TEST_SUITE_END()

// 另一测试套件: palindrome_tests
BOOST_AUTO_TEST_SUITE(palindrome_tests)

// ...

// 结束另一测试套件
BOOST_AUTO_TEST_SUITE_END()

Catch2 测试框架

Bugfix

cmake-examples/05-unit-testing/catch2-vendored/3rd_party/catch2/catch2/catch.hpp 中发现系统依赖性漏洞。

如果您在构建代码时出现 constexpr static std::size_t sigStackSize 常量定义失败,进行如下修改:

1
2
3
4
5
6
7
// ----------- catch.hpp:8162 ----------
// 32kb for the alternate stack seems to be sufficient. However, this value
// is experimentally determined, so that's not guaranteed.
// --------------- 修改前 ---------------
constexpr static std::size_t sigStackSize = 32768 >= MINSIGSTKSZ ? 32768 : MINSIGSTKSZ;
// --------------- 修改后 ---------------
constexpr static std::size_t sigStackSize = 32768;

漏洞原因是 MINSIGSTKSZ 来自系统库,在一些系统中它不是 constexpr 的。

下载 Catch2 测试框架

快速开始:Catch2: Get Started

获取单文件版本:catch2/catch.hpp

CMake 构建单元测试

这里我们假设你已经书写了 unit_tests.cpp Catch2 测试代码,我们会在后面介绍如何书写 Catch2 测试代码。

项目文件树:

文件依赖关系:

CMake 链接方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(unit-testing-catch2)

############################################################
# 构建
############################################################

# 构建测试单元一
# 这个项目比较简单, 实际直接构建测试程序即可, 但较复杂场景建议分离
add_library(Palindrome STATIC Palindrome.cpp)
target_include_directories(Palindrome
PUBLIC
${PROJECT_SOURCE_DIR}/
)

# 构建测试单元二
# 这个项目比较简单, 实际直接构建测试程序即可, 但较复杂场景建议分离
add_library(Reverse STATIC Reverse.cpp)
target_include_directories(Reverse
PUBLIC
${PROJECT_SOURCE_DIR}/
)

############################################################
# 测试
############################################################

# 构建测试程序
add_executable(unit_tests unit_tests.cpp)
target_link_libraries(unit_tests
PRIVATE
Palindrome # 链接测试单元一
Reverse # 链接测试单元二
Catch2::Test # 链接 Catch2 测试框架
)

# 将测试注册到 CTest
enable_testing()
add_test(test_all unit_tests)

# 构建子项目 Catch2::Test
add_subdirectory(${PROJECT_SOURCE_DIR}/3rd_party/catch2/)

1
2
3
4
5
6
7
8
9
# CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(catch2)

# Prepare "Catch2" library for other executables
add_library(Catch2 INTERFACE)
add_library(Catch2::Test ALIAS Catch2)
target_include_directories(Catch2 INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

运行单元测试与调试

凡是 cmake 构建的测试程序调试方式其实都一样。

1
2
3
4
5
6
7
8
9
10
11
12
# 创建 build 目录
mkdir -p build
cd build

# 构建可调试测试程序
cmake .. -D CMAKE_BUILD_TYPE=Debug
make

make test # 使用 CTest 测试, 确保测试已注册
./unit_tests # 直接运行测试
gdb unit_tests # 使用 gdb / lldb / etc. 调试
# ... # 或者使用 IDE 测试 (推荐, 自己配置或者使用 CMake 插件)

书写 Catch2 单元测试

需要注意的是单元测试框架都不能使用 main 函数,因为框架本质就是为用户提供支持测试的 main 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define CATCH_CONFIG_MAIN  // 为用户生成 main 函数
#include "catch2/catch.hpp"

TEST_CASE("simple") {
// ...
REQUIRE( res == "olleH" ); // 如果执行错误, 不再执行下一语句
// ...
}

TEST_CASE("empty") {
// ...
REQUIRE( res == "" ); // 如果执行错误, 不再执行下一语句
// ...
}

GoogleTest 测试框架(推荐)

这里我们假设你已经书写了 unit_tests.cpp GoogleTest 测试代码,我们会在后面介绍如何书写 GoogleTest 测试代码。

源代码构建单元测试

项目文件树:

文件依赖关系:

CMake 链接方式:

只需克隆官方源代码即可同时获取第三方 CMakeLists.txt:

项目源地址:Google Testing and Mocking Framework - Github

更多构建细节:GoogleTest: Generic Build Instructions

1
2
3
4
cd ./3rd_party/
git clone https://github.com/google/googletest.git
cat ./googletest/CMakeLists.txt # 查看第三方 CMakeLists.txt
# 此时第三方 CMakeLists.txt 连同源代码均下载到 ./3rd_party/googletest/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(unit-testing-googletest)

############################################################
# 构建
############################################################

# 构建测试单元一
# 这个项目比较简单, 实际直接构建测试程序即可, 但较复杂场景建议分离
add_library(Palindrome STATIC Palindrome.cpp)
target_include_directories(Palindrome PUBLIC ${PROJECT_SOURCE_DIR})

# 构建测试单元二
# 这个项目比较简单, 实际直接构建测试程序即可, 但较复杂场景建议分离
add_library(Reverse STATIC Reverse.cpp)
target_include_directories(Reverse PUBLIC ${PROJECT_SOURCE_DIR})

############################################################
# 测试
############################################################

# 构建测试程序
add_executable(unit_tests unit_tests.cpp)
target_link_libraries(unit_tests
PRIVATE
Palindrome # 链接测试单元一
Reverse # 链接测试单元二
gtest_main # 链接 GoogleTest 测试框架
)

# 将测试注册到 CTest
enable_testing()
add_test(test_all unit_tests)

# 构建子项目 gtest / gtest_main / gmock / gmock_main
add_subdirectory(${PROJECT_SOURCE_DIR}/3rd_party/googletest/)

自动下载构建单元测试

建议先阅读"源代码构建单元测试",我们在其中介绍了演示项目结构。

CMake 链接方式:

通过 CMake 提供的 FetchContent 模块自动获取,这是 GoogleTest 官方推荐的方式;不过同时我加了重复下载检测。

项目源地址:Google Testing and Mocking Framework - Github

更多构建细节:GoogleTest: Generic Build Instructions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(unit-testing-googletest-deps)

set(DOWNLOAD_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src/)
set(DOWNLOAD_BINARY_DIR ${PROJECT_SOURCE_DIR}/bin/)

# 避免重复下载
# 库损坏时删除并重新构建, 或使用 -D ALWAYS_DOWNLOAD=TRUE 强制下载并构建.
if(NOT "${ALWAYS_DOWNLOAD}" AND EXISTS ${DOWNLOAD_SOURCE_DIR} AND EXISTS ${DOWNLOAD_BINARY_DIR})
# 从下载目录构建
message("Download Task GoogleTest is skipped.")
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
add_subdirectory(${DOWNLOAD_SOURCE_DIR} ${DOWNLOAD_BINARY_DIR})
else()
# 摘自 https://github.com/google/googletest/blob/main/googletest/README.md
include(FetchContent)
FetchContent_Declare(googletest
# Specify the commit you depend on and update it regularly.
URL https://github.com/google/googletest/archive/5376968f6948923e2411081fd9372e71a59d8e77.zip
SOURCE_DIR ${DOWNLOAD_SOURCE_DIR}
BINARY_DIR ${DOWNLOAD_BINARY_DIR}
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
endif()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(unit-testing-googletest)

############################################################
# 构建
############################################################

# 构建测试单元一
# 这个项目比较简单, 实际直接构建测试程序即可, 但较复杂场景建议分离
add_library(Palindrome STATIC Palindrome.cpp)
target_include_directories(Palindrome PUBLIC ${PROJECT_SOURCE_DIR})

# 构建测试单元二
# 这个项目比较简单, 实际直接构建测试程序即可, 但较复杂场景建议分离
add_library(Reverse STATIC Reverse.cpp)
target_include_directories(Reverse PUBLIC ${PROJECT_SOURCE_DIR})

############################################################
# 测试
############################################################

# 构建测试程序
add_executable(unit_tests unit_tests.cpp)
target_link_libraries(unit_tests
PRIVATE
Palindrome
Reverse
gtest_main
)

# 将测试注册到 CTest
enable_testing()
add_test(test_all unit_tests)

# 构建子项目 gtest / gtest_main / gmock / gmock_main
add_subdirectory(${PROJECT_SOURCE_DIR}/3rd_party/googletest/)

运行单元测试与调试

凡是 cmake 构建的测试程序调试方式其实都一样。

1
2
3
4
5
6
7
8
9
10
11
12
# 创建 build 目录
mkdir -p build
cd build

# 构建可调试测试程序
cmake .. -D CMAKE_BUILD_TYPE=Debug
make

make test # 使用 CTest 测试, 确保测试已注册
./unit_tests # 直接运行测试
gdb unit_tests # 使用 gdb / lldb / etc. 调试
# ... # 或者使用 IDE 测试 (推荐, 自己配置或者使用 CMake 插件)

书写 GoogleTest 单元测试

需要注意的是单元测试框架都不能使用 main 函数,因为框架本质就是为用户提供支持测试的 main 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <gtest/gtest.h>

GTEST_TEST(PalindromeTests, is_palindrome) {
// ...
EXPECT_TRUE(pally.isPalindrome(pal)); // 检查: 即使执行错误, 依然执行下一语句
// ...
}

class ReverseTests : public ::testing::Test {};

GTEST_TEST_F(ReverseTests, simple) {
// ...
EXPECT_EQ(res, "olleH" ); // 检查: 即使执行错误, 依然执行下一语句
// ...
}

GTEST_TEST_F(ReverseTests, empty) {
// ...
ASSERT_EQ(res, ""); // 断言: 如果执行错误, 不再执行下一语句
// ...
}


安装与打包

CMake 构建目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
cmake_minimum_required(VERSION 3.5)

project(cmake_examples_deb)

# set a project version
set (deb_example_VERSION_MAJOR 0)
set (deb_example_VERSION_MINOR 2)
set (deb_example_VERSION_PATCH 2)
set (deb_example_VERSION "${deb_example_VERSION_MAJOR}.${deb_example_VERSION_MINOR}.${deb_example_VERSION_PATCH}")


############################################################
# Create a library
############################################################

#Generate the shared library from the library sources
add_library(cmake_examples_deb SHARED src/Hello.cpp)

target_include_directories(cmake_examples_deb
PUBLIC
${PROJECT_SOURCE_DIR}/include
)
############################################################
# Create an executable
############################################################

# Add an executable with the above sources
add_executable(cmake_examples_deb_bin src/main.cpp)

# link the new hello_library target with the hello_binary target
target_link_libraries( cmake_examples_deb_bin
PUBLIC
cmake_examples_deb
)

############################################################
# Install
############################################################

# Binaries
install (TARGETS ${PROJECT_SOURCE_DIR}/cmake_examples_deb_bin
DESTINATION bin/)

# Library
# Note: may not work on windows
install (TARGETS ${PROJECT_SOURCE_DIR}/cmake_examples_deb
LIBRARY DESTINATION lib/)

# Config
install (FILES ${PROJECT_SOURCE_DIR}/cmake-examples.conf
DESTINATION etc/)

# Include
install(
DIRECTORY ${PROJECT_SOURCE_DIR}/include/
DESTINATION include/
)

############################################################
# Create DEB
############################################################

# Tell CPack to generate a .deb package
set(CPACK_GENERATOR "DEB")

# Set a Package Maintainer.
# This is required
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Thom Troy")

# Set a Package Version
set(CPACK_PACKAGE_VERSION ${deb_example_VERSION})

# Include CPack
include(CPack)

安装与打包

1
2
3
4
5
6
7
8
9
10
11
# 创建 build 目录
mkdir -p build
cd build

# 构建主程序
cmake ..
make

make install # 安装
make package # 打包为 .deb