《CMake Best Practice》第 3 章笔记--Creating a CMake Project
初始化项目
CMake
项目推荐的文件组织方式:
build
:构建目录,用来存放build
文件和二进制文件。include/project_name
:存放所有项目外可以公开访问的头文件。因为通常include
一个头文件的形式是这样的#include <project_name/somefile.h>
,所以有必要在include
文件夹下创建一个project_name
子文件夹,这样外部引用头文件时,可以很容易分辨出来这个头文件所属的项目。src
:存放所有私有的.h
头文件和.cpp
源文件。CMakeLists.txt
:CMake
根文件。
嵌套项目
当将项目相互嵌套时,每个项目都应该映射上面的文件结构,并且为每个子项目编写 CMakeLists.txt
,以便子项目可以独立构建。
每个子项目的 CMakeLists.txt
都应该包含 cmake_minimum_required
和 project
指令。
一个嵌套项目可能有下面的文件结构:
创建一个hello world可执行文件
src
文件夹下只包含一个 main.cpp
文件:
#include <iostream>
int main(int, char **) {
std::cout << "Welcome to CMake Best Practices\n";
return 0;
}
CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.21)
project(
hello_world_standalone
VERSION 1.0
DESCRIPTION "A simple C++ project"
HOMEPAGE_URL https://github.com/PacktPublishing/CMake-Best-Practices
LANGUAGES CXX
)
add_executable(hello_world)
target_sources(hello_world PRIVATE src/main.cpp)
target_sources
指定了可执行文件 hello_world
的源文件。因为 CMake
中的各个 target
之间可能会有依赖关系,因此 PRIVATE
的作用是:
PRIVATE defines that the sources are only used to build this target and not for any dependent targets.
除非你正在构建一个巨大的单体应用程序,否则应该使用 library
来模块化和分发代码。
创建一个库
cmake_minimum_required(VERSION 3.21)
project(
ch3.hello_lib
VERSION 1.0
DESCRIPTION "A simple C++ project to demonstrate creating executables and libraries in CMake"
LANGUAGES CXX)
add_library(hello)
target_sources(hello PRIVATE src/hello.cpp src/internal.cpp)
target_compile_features(hello PUBLIC cxx_std_17)
target_include_directories(
hello
PRIVATE src/hello
PUBLIC include)
在 add_library
指令中应该传递 STATIC
或者 SHARED
来指定库的类型。但是如果没有显式的传递,那么该库的类型由用户通过设置 BUILD_SHARED_LIBS
变量的值来指定库的类型。注意:该变量只能由用户传递,不可以在 CMake
文件中指定。
target_sources
指令中使用 PRIVATE
或者 PUBLIC
或者 INTERFACE
指定源文件的可见性。通常源文件应该指定成 PRIVATE
。它们三者之间的区别是:
PRIVATE
,源文件只会被用来编译hello
库自己。PUBLIC
,源文件不仅被用来编译hello
自己,还会被用来编译任何一个链接了hello
库的target
。INTERFACE
,源文件不会被用来编译hello
库自己,但是会被用来编译任何一个链接了hello
库的target
。
使用 target_include_directories
设置 hello
库的 include
目录。
共享库的符号可见性
不同的编译器对共享库的符号可见性有不同的处理方法,gcc
和 clang
把所有的符号都当成可见的,除非显式的指定,否则 MSVC
会把所有符号都隐藏。
改变默认的可见性
如果需要改变默认的可见性,需要把 <LANG>_VISIBILITY_PRESET
属性设置为 HIDDEN
,可以针对全局设置该属性,也可以针对某一个具体的 target
设置该属性,对于不同的语言,<LANG>
应该是不同的值,比如 CXX
或者 C
。
设置完这个属性之后,除非特殊指定,否则所有的符号都是默认隐藏的。可以在符号之前添加一个预处理器定义(其实就是一个宏定义),改变这个符号的可见性,比如:
class HELLO_EXPORT Hello {
…
};
CMAKE
提供了 generate_export_header
指令来生成这个预处理器定义(如 HELLO_EXPORT),使用这个指令之前,必须要先 include
GenerateExportHeader
这个模块。
看一个例子:
add_library(hello SHARED)
set_property(TARGET hello PROPERTY CXX_VISIBILITY_PRESET "hidden")
set_property(TARGET hello PROPERTY VISIBILITY_INLINES_HIDDEN TRUE)
include(GenerateExportHeader)
generate_export_header(hello EXPORT_FILE_NAME export/hello/export_hello.hpp)
target_include_directories(hello PUBLIC "${CMAKE_CURRENT_BINARY_DIR} /export")
将 VISIBILITY_INLINES_HIDDEN
属性设置为 TRUE
来隐藏内联成员函数。
generate_export_header
指令将在 CMAKE_CURRENT_BINARY_DIR/export/hello
文件夹下生成一个 export_hello.hpp
文件,当你想将一个符号设置为可见时,应该 #include <hello/export_hello.hpp>
。
更多的参考,可见:https://cmake.org/cmake/help/latest/module/GenerateExportHeader.html
仅包含头文件的库(header-only library)
创建一个仅包含头文件的库的实例如下:
project(ch3_hello_header_only VERSION 1.0
DESCRIPTION "Chapter 3 header-only example"
LANGUAGES CXX)
add_library(hello_header_only INTERFACE)
target_include_directories(hello_header_only INTERFACE include/)
target_compile_features( hello_header_only INTERFACE cxx_std_17)
在 add_library
指令中需要将仅包含头文件的库指定为 INTERFACE
,因为 header-only library
不需要被编译,所以它也没有 source list
,自然不需要target_sources()
指令。
对象库
使用对象库可以将你的代码分割成一个个可重用的部分。举个例子,你可能在一个可执行文件和一个单元测试用例中都用到了同一个库的代码,无论是使用静态库还是动态库,你都需要编译这个库两次。对象库可以避免代码的多次编译,因为对象库只会将代码进行编译,不会执行链接动作。
从 CMAKE 3.12
开始,你可以像使用普通的库一样使用对象库,你可以直接使用 target_link_libraries
来链接一个对象库。
使用库
add_subdirectory(hello_lib)
add_subdirectory(hello_header_only)
add_subdirectory(hello_object)
add_executable(chapter3)
target_sources(chapter3 PRIVATE src/main.cpp)
target_link_libraries(chapter3 PRIVATE hello_header_only hello hello_object)
使用 target_link_libraries
来给一个 target
链接其它的库。在链接时同样有三种访问控制符:
PRIVATE
,库只会被用来链接chapter3
可执行文件自己。PUBLIC
,库不仅被用来链接自己,还会被用来链接任何一个链接了hello
库的target
。INTERFACE
,源文件不会被用来链接库自己,但是会被用来链接任何一个链接了hello
库的target
。
设置编译器和链接器选项
分别使用 target_compile_options
和 target_link_options
来设置编译器和链接器的选项。
但是不同的编译器传递选项的方式不同,比如 GCC
和 Clang
用 -
传递参数,而 MSVC
用 /
传递参数。使用生成器表达式可以很好的解决这个问题。比如下面的例子:
target_compile_options(hello PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/SomeOption>
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-someOption>
)
关于生成器表达式可以看这篇文章:https://zhuanlan.zhihu.com/p/437404485
添加预处理器定义
使用 target_compile_definitions
添加预处理器定义。
比如,为源代码添加一个宏定义:
add_compile_definitions(MG_ENABLE_OPENSSL=1)
这相当于:
#define MG_ENABLE_OPENSSL 1