完整的C++项目构建注意事项

最近常常使用cmake构建c++项目有感,从创建项目到打包发布总结一下需要注意的事情.

项目组织方式

具体的项目组织方式因人而异,这里推荐一种,在src目录中创建模块目录,再在include目录中常见对应的同名目录包含头文件,可执行程序的源代码或者最终生成库的源代码可以放在app目录中.

比如我看的一个项目组织如图

  1. 在src目录中包括demo,view,assignment三个项目,对应include目录相同,或者在生成程序的目录中包含头文件而不另外放include中.

image-20240916205927734

  1. 此外也有src目录中放所有的源代码文件,include目录分别放每个模块对应的头文件,相对来说更方便.

针对第一种组织方式,cmake会在src目录添加模块

1
2
3
add_subdirectory(view)
add_subdirectory(demo)
add_subdirectory(assignments)

每个模块再单独写cmake,甚至可以单独写project,这样方便模块化,可以看到下面利用不同的${PROJECT_NAME}设置库生成位置

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
project(demo_hello_world)
file(GLOB source
"${CMAKE_CURRENT_SOURCE_DIR}/demo_hello_world.cpp"
)
add_executable(${PROJECT_NAME} ${source})
set_target_properties(${PROJECT_NAME} PROPERTIES
DEBUG_POSTFIX "_d"
RUNTIME_OUTPUT_DIRECTORY "${BINARY_DIR}"
LIBRARY_OUTPUT_DIRECTORY "${LIBRARY_DIR}"
ARCHIVE_OUTPUT_DIRECTORY "${LIBRARY_DIR}")
target_link_libraries(${PROJECT_NAME} PUBLIC view)

project(demo)
file(GLOB source
"${CMAKE_CURRENT_SOURCE_DIR}/demo.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/window_demo.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/window_demo.h"
)
add_executable(${PROJECT_NAME} ${source})
set_target_properties(${PROJECT_NAME} PROPERTIES
DEBUG_POSTFIX "_d"
RUNTIME_OUTPUT_DIRECTORY "${BINARY_DIR}"
LIBRARY_OUTPUT_DIRECTORY "${LIBRARY_DIR}"
ARCHIVE_OUTPUT_DIRECTORY "${LIBRARY_DIR}")
target_link_libraries(${PROJECT_NAME} PUBLIC view)

比较来看,如果给src目录中放所有的cpp源文件,那不好给模块分离,因为一个项目中一般包括一个生成可执行程序或最终库的源代码,一起一堆供这个目标依赖的模块,这些模块如果能单独提出来更好,也就是说这些模板的cpp代码如果放在分别的模块目录下虽然更麻烦但更好. 此外将main程序放在app目录中也更加清晰.

依赖图与文档生成

查看目标的依赖

下载graphviz

1
cd build && cmake .. --graphviz=graph.dot && dot -Tpng graph.dot -o graphImage.png

graphImage

使用Doxygen生成文档

需要按照规定格式撰写注释,根据注释生成文档Doxygen: Documenting the code.

doxygen支持许多格式注释,下面列举三种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* ... text ...
*/

/** Brief description which ends at this dot. Details follow
* here.
*/

/*!
* ... text ...
*/

/*! \brief Brief description.
* Brief description continued.
*
* Detailed description starts here.
*/

/*!
... text ...
*/
注释介绍
@file文件说明
@author作者的信息
@brief用于class 或function的批注中,后面为class 或function的简易说明
@param参数介绍
@return函数传回值的说明

Doxygen 还需要一个 Doxyfile,其包含文档生成的所有参数,比如输出格式、排除的文件模式、 项目名称等。因为配置参数太多,开始配置 Doxygen 可能会让人望而生畏,但 CMake 可以自动生 成 Doxyfile。

1
doxygen -g # 生成doxyfile

配置doxygenfile然后运行doxygen生成.

当然更好的方式是结合cmake,首先找到doxygen程序,然后设置需要的选项Doxygen: Configuration,最后生成文旦. 可以使用add_custom_target或者doxygen_add_docs(推荐)FindDoxygen — CMake 3.30.3 Documentation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
find_package(Doxygen)
if (DOXYGEN_FOUND)
set(DOXYGEN_OUTPUT_DIRECTORY"${CMAKE_CURRENT_BINARY_DIR}/docs")
set(DOXYGEN_GENERATE_HTML YES)
set(DOXYGEN_GENERATE_MAN YES)
set(DOXYGEN_MARKDOWN_SUPPORT YES)
set(DOXYGEN_AUTOLINK_SUPPORT YES)
set(DOXYGEN_HAVE_DOT YES)
set(DOXYGEN_COLLABORATION_GRAPH YES)
set(DOXYGEN_CLASS_GRAPH YES)
set(DOXYGEN_UML_LOOK YES)
set(DOXYGEN_DOT_UML_DETAILS YES)
set(DOXYGEN_DOT_WRAP_THRESHOLD 100)
set(DOXYGEN_CALL_GRAPH YES)
set(DOXYGEN_QUIET YES)
#add_custom_target(docs ${DOXYGEN_EXECUTABLE} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/docs))
doxygen_add_docs(
docs
"${CMAKE_CURRENT_LIST_DIR}"
ALL
COMMENT "Generating documentation for myproject"
)
endif()

1
2
3
4
5
6
7
doxygen_add_docs(targetName
[filesOrDirs...]
[ALL]
[USE_STAMP_FILE]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[CONFIG_FILE filename])

第一个参数 targetName 是文档目标的名称,该函数将生成一个名为 targetName 的自定义目 标。这个目标将触发 Doxygen,并在构建时使用代码创建文档。

filesOrDirs包含想要从文档生成的代码的文件或目录的列表。

ALL 参数用于使 CMake 的 ALL 元目标依赖于 doxygen_add_docs(…) 创建的文档目标,因此在构建 ALL 元目标时自动生成文档。

WORKING_DIRECTORY默认是 CMAKE_CURRENT_SOURCE_DIR

代码检查和格式化工具

这部分工作其实完全可以交由IDE提供,不需要在cmake build时使用的,但为了保持兼容,这里简略写一点

可以考虑使用clang-tidyclang-format工具,在cmake文件中

1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.28)
project(my-project)

add_executable(my-app main.c)

file(GLOB_RECURSE ALL_SOURCE_FILES
*.c *.h *.cpp *.hpp *.cxx *.hxx *.cc *.hh *.cppm *.ipp *.ixx)
add_custom_target(format
COMMAND clang-format
-i
${ALL_SOURCE_FILES}
)

对于clang-tidy完全可以在.clang-tidy文件中设置并通过clangd进行检查Enabling clang-tidy checks in clangd - Clang Frontend / clangd - LLVM Discussion Forums

Configuration (llvm.org)

1
2
3
4
5
# .clangd
Diagnostics:
ClangTidy:
CheckOptions:
readability-identifier-naming.VariableCase: CamelCase

此外还可以设置编译器编译链接选项检查内存和初始化等错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function (add_sanitier target)
message(STATUS "Adding sanitizer to target ${target}")
if (CMAKE_CXX_COMPILER_ID MATCHES "CLANG" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU")
add_compile_options("-fno-omit-frame-pointer")
add_link_options("-fno-omit-frame-pointer")
target_compile_options(${target} PRIVATE -fsanitize=address)
target_link_libraries(${target} PRIVATE -fsanitize=address)
target_compile_options(${target} PRIVATE -fsanitize=undefined)
target_link_libraries(${target} PRIVATE -fsanitize=undefined)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_compile_definitions(${target} PRIVATE /fsanitize=address)
else()
message(WARNING "Sanitier is not supported for ${CMAKE_CXX_COMPILER_ID}")
endif()
endfunction()

除此之外,可以使用valgrind等工具动态debug查找内存问题.

进行测试

CTest

Testing With CMake and CTest — Mastering CMake

1
2
3
4
5
include(CTest)
add_executable(TestInstantiator TestInstantiator.cxx)
target_link_libraries(TestInstantiator vtkCommon)
add_test(NAME TestInstantiator
COMMAND TestInstantiator)
1
make test

CTest 模块通常应该只包含在项目的顶层 CMakeLists.txt 中。自从 CMake 版本 3.21 以 来,PROJECT_IS_TOP_LEVEL 可以用来测试当前的 CMakeLists.txt 是否为顶层文件。

对于项目的顶层目录和使用 ExternalProject 添加的项目顶层目录,此变量为 True。对于使用 add_subdirectory 或 FetchContent 添加的目录,该值为 False

1
2
3
4
5
project(CMakeBestPractice)
...
if(PROJECT_IS_TOP_LEVEL)
include(CTest)
endif()
1
2
3
4
add_test(NAME <name> COMMAND <command> [<arg>...]
[CONFIGURATIONS <config>...]
[WORKING_DIRECTORY <dir>]
[COMMAND_EXPAND_LISTS])
1
2
3
ctest --test-dir <build_dir>
cmake --build <build_dir> --target test # 注意这里目标就是test,而不是add_test中添加的NAME
ctest --build-and-test <source_dir> <build_dir>

可以设置ctest多个lable,然后通过过滤查看对应结果

1
2
3
4
5
6
add_test(NAME labeled_test_1 COMMAND someTest)
set_tests_properties(labeled_test PROPERTIES LABELS "example")
add_test(NAME labeled_test_2 COMMAND anotherTest)
set_tests_properties(labeled_test_2 PROPERTIES LABELS "will_fail" )
add_test(NAME labeled_test_3 COMMAND YetAnotherText)
set_tests_properties(labeled_test_3 PROPERTIES LABELS "example;will_fail")
1
ctest -L "example|will_fail"

-L进行过滤

1
ctest -I [Start,End,Stride,test#,test#,...|Test file] 

通过 Start、End 和 Stride,可以指定要执行的测试的范围。这三个数字是与显式测试数字 test# 相结合的范围,或传递包含参数的文件

处理大量测试

1
2
3
4
5
6
create_test_sourcelist (SourceListName
DriverName
test1 test2 test3
EXTRA_INCLUDE include.h
FUNCTION function
)

注意ctest并不提供方便测试的方法,可以使用第三方库提供的REQUIRE等方法

使用include(Ctest)add_test可使得可以方便使用ctest命令进行测试

比如

1
2
cmake --build build
cd build && ctest

使用Catch2

catchorg/Catch2: A modern, C++-native, test framework for unit-tests, TDD and BDD - using C++14, C++17 and later (C++11 support is in v2.x branch, and C++03 on the Catch1.x branch) (github.com)

创建tests目录,编写cmake文件

1
2
3
4
5
6
7
8
9
10
if(ENABLE_TESTING)

set(TEST_MAIN "unit_tests")
set(TEST_SOURCES main.cpp)
set(TEST_INCLUDES "./")

add_executable(${TEST_MAIN} ${TEST_SOURCES})
target_include_directories(${TEST_MAIN} PUBLIC ${TEST_INCLUDES})
target_link_libraries(${TEST_MAIN} PUBLIC ${LIBRARY_NAME} Catch2::Catch2WithMain)
endif()

自动发现测试

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.5)
project(baz LANGUAGES CXX VERSION 0.0.1)
find_package(Catch2 REQUIRED)
add_executable(tests test.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2)
# list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) # use FetchContent
include(CTest)
include(Catch)
catch_discover_tests(tests)

使用GoogleTest

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.14)
project(my_project)

# GoogleTest requires at least C++14
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()

add_executable(hello_test hello_test.cpp)
target_link_libraries(hello_test GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(hello_test) # 自动发现测试

代码覆盖检查

检查测试了哪些代码并生成覆盖率报告,使用Gcov生成覆盖率信息,使用覆盖分析程序,如 Gcovr 或 LCOVn分析覆盖文件并生成报告

小结

CTest+Catch2即可

第三方库管理

使用FetchContent下载库

1
2
3
4
5
6
include(FetchContent)
FetchContent_Declare(nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json
GIT_TAG v3.11.2
GIT_SHALLOW TRUE)
FetchContent_Makeavailable(nlohmann_json)

需要项目是cmake项目

使用vcpkg等包管理工具下载库

包管理器与xmake介绍

1
2
3
4
5
6
7
8
9
10
{
"version": 6,
"configurePresets": [
{
"name": "my-preset",
"binaryDir": "${sourceDir}/build",
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
}
]
}
1
2
3
4
5
6
cmake_minimum_required(VERSION 3.28)
project(my-project)
find_package(ftxui REQUIRED)
add_executable(my-app main.cpp)
target_compile_features(my-app PRIVATE cxx_std_20)
target_link_libraries(my-app PRIVATE ftxui::dom ftxui::screen ftxui::component)
1
2
3
4
vcpkg install
cmake --preset my-preset
cmake --build build
./build/my-app

使用Conan

简单介绍一下使用流程

首先定义conanfile.txt和conan profile

1
2
3
4
5
6
[requires]
zlib/1.2.11

[generators]
CMakeDeps
CMakeToolchain
1
conan profile detect --force

然后执行conan insatll会生成conan_toolchain.cmake

1
conan install . --output-folder=build --build=missing

再在cmake中使用

1
2
cd build
cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"

借助git submodule下载库

Git - git-submodule Documentation

Git submodule | Atlassian

1
2
3
4
git submodule add https://bitbucket.org/jaredw/awesomelibrary # 添加子模块(当前新版本git也会下载对应模块)
git submodule init //初始化子模块 (根据.gitmodules更新信息)
git submodule update //更新子模块
git submodule update --init --recursive # 更新映射关系并递归下载模块

image-20240917150517478

使用git submodule add之后会创建.gitmodules文件并写入相关信息,包括子模块path和url,其中path是安装路径,因此我们可以借助修改path,使得git update --init安装子模块时安装到3rd_party或vendor目录便于管理,比如

1
2
3
[submodule "glfw"]
path = third_party/glfw
url = https://github.com/glfw/glfw.git

修改.gitmodules 文件中对应模块的名字或者path,然后使用git submodule sync进行更新.

1
2
git submodule sync --recursive
git submodule update --init --recursive

此外还会在.git/config.git/modules中添加子模块信息

update的作用是根据项目的配置信息,拉取更新子模块中的代码,也可以使用git clone --recurse-submodules直接下载子模块

卸载子模块

1
2
git submodule deinit project-sub # 在.gitmodules中对应的模块名 
git rm project-sub # 删除模块目录与.git/config,.git/modules信息

总结来说,可以使用第三方管理工具,下载链接非常方便. 对于自己写的一些库或者没有cmake的项目可以使用vendor/3rd_party方式,放在一个单独目录,如果是源代码,添加源文件和头文件,生成库,cmake如下

1
2
3
4
5
6
7
8
9
10
# glad
set(glad_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/glad" CACHE STRING "")
file(GLOB source
"${glad_SOURCE_DIR}/src/*.c"
)
add_library(glad ${source})
target_include_directories(glad
PUBLIC "${glad_SOURCE_DIR}/include"
)
set_target_properties(glad PROPERTIES FOLDER "third_party")

如果是已经编译好的库,使用add_library(xxx SHARED IMPORTED)并设置库文件位置

1
2
3
4
5
add_library(glad SHARED IMPORTED)
set_target_properties(glad PROPERTIES IMPORTED_LOCATION "/path/to/glad/library")
set_target_properties(glad PROPERTIES IMPORTED_IMPLIB "/path/to/glad/library") # 针对windows

set_target_properties(glad PROPERTIES FOLDER "third_party")

IMPORTED_IMPLIB:用于指定导入库的导入库文件(import library file)。在 Windows 上,通常用于 .lib 文件。这个属性通常用于静态链接的导入库

小结

vcpkg,conan的逻辑是使用一个文件声明项目信息和依赖,然后在cmake中添加toolchainfile用于下载对应的包,而CPM和FetchContent直接在cmake中声明需要添加的包.

优先使用第三方包管理工具,因为相比FetchContent提供更多功能,如果第三方库不是cmake项目,使用git submodule方式,下载到某个文件夹编译源代码、链接库

项目打包、安装与分发

install

安装target

具体来说install(TARGETS …)会安装生成的的东西,不会安装头文件或者项目中的json、txt等读取文件.

1
install(TARGETS <target>... [...])

image-20240918153002484

这里最需要注意的就是动态库不包括windows上的dll.

默认安装路径如下,安装目录在Unix上usr/local,Windows是C:/program files,前缀通过cmake --prefixCMAKE_INSTALL_PREFIX指定

image-20240918160115134

install(TARGETS…) 如果 包 含 EXPORT 参 数, 用 于 从 给 定 的 install(…) 目 标 创 建 一 个 导 出 名 称,可以使用此导出名称导出这些目标

安装文件

安装的东西并不总是目标输出构件的一部分。它们可能是目标的运行时依赖项,例如图片、源文件、脚本和配置文件

1
2
3
4
install (
DIRECTORY include/
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
)

install(FILES…) 指令接受一个或多个文件作为参数,TYPE 和DESTINATION 用于确定指定文件的目标目录。TYPE 用于指示哪些文件将使用该文件类型的默认 路径作为安装目录

image-20240918155717561

1
2
3
4
5
install(FILES "${CMAKE_CURRENT_LIST_DIR}/greeter_content"
DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(PROGRAMS "${CMAKE_CURRENT_LIST_DIR}/greeter.py"
DESTINATION "${CMAKE_INSTALL_BINDIR}" RENAME chapter4_greeter)

安装目录

1
2
3
4
5
6
install(DIRECTORY dir1 dir2 dir3 TYPE LOCALSTATE)

install(DIRECTORY dir2 DESTINATION ${CMAKE_INSTALL_
LOCALSTATEDIR} FILES_MATCHING PATTERN "*.hpp"
EXCLUDE PATTERN "*")

可以指定匹配文件和排除文件模式.

config-file

当别人安装了你的库,也要方便使用.为了让其他用户使用find_package找到我们的包,需要config-file.

包配置文件Config.cmake设置如下

1
2
3
include(GNUInstallDirs) # 便于获取安装路径变量
set(FOO_INCLUDE_DIRS ${PREFIX}/include/foo-1.2)
set(FOO_LIBRARIES ${PREFIX}/lib/foo-1.2/libfoo.a)

搜索包时,find_package(…) 会查找 /cmake 目录,所以包配置文件放在/cmake中.

1
2
3
4
# top level cmake
include(GNUInstallDirs)
set(project_INSTALL_CMAKEDIR cmake CACHE PATH
"Installation directory for config-file package cmake files") # 于设置 config-file 打包配置文件的安装目录
1
2
3
4
target_include_directories(ch4_ex05_lib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_compile_features(ch4_ex05_lib PUBLIC cxx_std_11)

使用$设置头文件目录,因为在安装时

1
2
3
4
5
6
7
8
install(TARGETS ex05_lib
EXPORT cex05_lib_export
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install (
DIRECTORY ${PROJECT_SOURCE_DIR}/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

使用install(EXPORT)得到xxConfig.cmake文件

1
2
3
4
5
install(EXPORT ex05_lib_export
FILE ex05_lib-config.cmake
NAMESPACE ex05_lib::
DESTINATION ${project_INSTALL_CMAKEDIR}
)

要实现对 find_package(…) 的完全支持,还需要获取xxxConfig-version.cmake 文件

1
2
3
4
5
6
7
8
9
10
11
12
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"ex05_lib-config-version.cmake"
# Package compatibility strategy. SameMajorVersion is
essentially 'semantic versioning'.
COMPATIBILITY SameMajorVersion # 与主版本号相同即可
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/ex05_lib-config-version.
cmake"
DESTINATION "${project_INSTALL_CMAKEDIR}"
)
1
2
3
cmake –S . -B ./build
cmake --build ./build
cmake --install ./build --prefix /3rdparty

安装包并使用find_package使用包

1
2
3
4
5
6
7
8
9
10
if(NOT PROJECT_IS_TOP_LEVEL)
message(FATAL_ERROR "The chapter-4, ex05_consumer project is
intended to be a standalone, top-level project. Do not
include this directory.")
endif()
find_package(ex05_lib 1 CONFIG REQUIRED)
add_executable(ex05_consumer src/main.cpp)
target_compile_features(ex05_consumer PRIVATE cxx_std_11)
target_link_libraries(ex05_consumer ex05_lib::ch4_ex05_
lib)

image-20240918175420391

CPack

Packaging With CPack — Mastering CMake

cpack包含多种生成器生成包

image-20240918161939375

常用cpack变量,用于设置项目打包时的信息

image-20240918162023449

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.21)
project(
ch4_ex06_pack
VERSION 1.0
DESCRIPTION "Chapter 4 Example 06, Packaging with CPack"
LANGUAGES CXX)
if(NOT PROJECT_IS_TOP_LEVEL)
message(FATAL_ERROR "The chapter-4, ex06_pack project is
intended to be a standalone, top-level project.
Do not include this directory.")
endif()
add_subdirectory(executable)
add_subdirectory(library)
set(CPACK_PACKAGE_VENDOR "CTT Authors") # 作者
set(CPACK_GENERATOR "DEB;RPM;TBZ2") # 包管理器
set(CPACK_THREADS 0)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "CTT Authors")
include(CPack)

CPACKPACKAGE_NAME 和 CPACK_PACKAGE_VERSION* 默认从顶层项目名称和版本中获取

1
cmake –S . -B build/

项目配置后,生成CpackConfig.cmake 和 CpackConfigSource.cmake文件到build/CPack*中,使用cpack得到最终包

1
2
cmake --build build/
cpack --config build/CPackConfig.cmake -B build/

image-20240918164647416

注意设置generator时,其中每个都需要符合条件. 比如nsis需要安装对应的软件,否则设置generator包括它时会直接报错

经常遗忘的指令

1
2
3
4
5
6
7
8
9
10
add_custom_target(Name [ALL] [command1 [args1...]]
[COMMAND command2 [args2...] ...]
[DEPENDS depend depend depend ... ]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[JOB_POOL job_pool]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS]
[SOURCES src1 [src2...]])

add_custom_target 的核心是通过 COMMAND 选项传递的命令列表。虽然第一个命令可 以不带这个选项,但最好在 add_custom_target 中添加 COMMAND 选项。

默认情况下,定制目标只在显式请求时执行,除非指定了 ALL 选项

自定义目标总认为是过时的,因此总是运行指 定的命令,而不管是否会反复产生相同的结果。

使用 DEPENDS 关键字,可以使定制目标依赖于使用 add_custom_command 或其他目标定义的定制命令的文件和输出。

要使自定义目标依赖于另 一个目标,可以使用 add_dependencies。若使用自定义目标创建文件,可以在 BYPRODUCTS 选项下列出这些文件。

列出的文件都将使用 GENERATED 属性标记,CMake 使用该属性来确定构建是否过期,并找出需要清理的文件,但使用 add_custom_command 创建文件的任务可能更适 合。

通常,命令在当前二进制目录中执行,该目录在 CMAKE_CURRENT_BINARY_DIRECTORY 缓存变量中。若需要修改,这可以通过 WORKING_DIRECTORY 选项来更改。该选项可以是绝对 路径,也可以是相对路径 (当前二进制目录的相对路径)。

1
2
3
4
5
6
7
8
9
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]
[COMMAND_EXPAND_LISTS])

可以在以下时段将命令连接到构建中:

• PRE_BUILD: 在 Visual Studio 中,此命令在执行其他构建步骤之前执行。当使用其他生成器 时,会在 PRE_LINK 命令之前运行。

• PRE_LINK: 此命令将在编译源代码之后运行,在可执行文件或存档工具链接到静态库之前运行。

POS_BUILD: 这将在执行所有其他构建规则后运行该命令。 执行自定义步骤最常见的方法是使用 POST_BUILD; 其他两个选项很少使用,要么是因为支持 有限,要么是因为它们既不能影响链接,也不能影响构建。

1
2
3
4
5
cmake_parse_arguments(<prefix> <options> <one_value_keywords>
<multi_value_keywords> <args>...)

cmake_parse_arguments(PARSE_ARGV <N> <prefix> <options>
<one_value_keywords> <multi_value_keywords>)

cmake_parse_arguments — CMake 3.30.3 Documentation

Great resoureces for learning

  1. Effective Modern CMake (github.com)
  2. franneck94/CppProjectTemplate: C++ project template with unit-tests, documentation, ci-testing and workflows. (github.com)

书籍推荐CMake Best Practices (豆瓣) (douban.com)Professional CMake (豆瓣) (douban.com)

-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道