警告
本文最后更新于 2024-01-14,文中内容可能已过时。
1
2
3
4
5
6
7
| .
├── include
│ └── message.h
├── src
│ └── message.cpp
├── hello_world.cpp
└── CMakeLists.txt
|
项目结构是为了让我们开发人员对项目更加清晰,使代码结构更加清晰(模块化)。一般我们的项目比较简单时,可以构建为如上的项目结构。但是在构建大型项目时,项目结构会更加复杂,具体请参考下节内容。
这里我们构建了 include 目录和 src 目录,include 目录主要存放的是 CPP 文件的头文件,即函数的声明,为使用它的文件提供 API。src 目录主要是存放的函数的具体实现。
源码地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#ifndef MESSAGE_HEADER_H_
#define MESSAGE_HEADER_H_
#include <iostream>
#include <string>
class Message {
public:
Message() {}
void Print(const std::string &message);
};
#endif // ! MESSAGE_HEADER_H_
|
1
2
3
4
5
| #include "message.h"
void Message::Print(const std::string &message) {
std::cout << message << std::endl;
}
|
1
2
3
4
5
6
7
8
9
| #include "message.h"
int main() {
Message message;
message.Print("Hello, CMake World!");
message.Print("Goodbye, CMake World!");
return EXIT_SUCCESS;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(hello-world LANGUAGES CXX)
include_directories(
${CMAKE_SOURCE_DIR}/include
)
file(GLOB HEADER ${CMAKE_SOURCE_DIR}/include/*.h)
file(GLOB SOURCE ${CMAKE_SOURCE_DIR}/src/*.cpp)
add_executable(
${PROJECT_NAME}
${HEADER}
${SOURCE}
${CMAKE_SOURCE_DIR}/hello_world.cpp
)
|
代码释义:
Tip
1
2
3
| include_directories(
${CMAKE_SOURCE_DIR}/include
)
|
将 include
目录下的所有文件包含进来,这样 include 目录下的 message.h 将会被包含到整个项目中。如果我们在细分目录中使用包含某一模块的头文件,我们可以在具体模块的 CMakeLists.txt 中使用该命令,且要包含的头文件的可见性只有该模块,其他模块不可见,具体使用方法,请参考下节内容。
Tip
1
2
| file(GLOB HEADER ${CMAKE_SOURCE_DIR}/include/*.h)
file(GLOB SOURCE ${CMAKE_SOURCE_DIR}/src/*.cpp)
|
如果 include
目录和 src
目录中有多个头文件和源文件,使用如上命令可以将所有头文件集合到 HEADER 和 SOURCE 自定义宏定义中,使用时的命令为 ${HEADER}
和 ${SOURCE}
。
Tip
1
2
3
4
5
6
| add_executable(
${PROJECT_NAME}
${HEADER}
${SOURCE}
${CMAKE_SOURCE_DIR}/hello_world.cpp
)
|
这将结合 include
目录下的文件和 src 目录下的文件以及 hello_world.cpp
生成名为 hello-world
的可执行文件。
1
2
3
| mkdir build
cd build
cmake ..
|
构建过程:
1
2
3
4
5
6
7
8
9
10
| -- The CXX compiler identification is GNU 9.4.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/jiangli/repo/tutorials/cmake-tutorial/chapter1/02/build
|
构建可执行文件并输出:
1
2
3
4
5
| Scanning dependencies of target hello-world
[ 33%] Building CXX object CMakeFiles/hello-world.dir/src/message.cpp.o
[ 66%] Building CXX object CMakeFiles/hello-world.dir/hello_world.cpp.o
[100%] Linking CXX executable hello-world
[100%] Built target hello-world
|
上一篇我们没有讲将执行 cmake 命令后生成的 MakeFile 文件,其如何构建出可执行文件的具体操作,只是简单的说 MakeFile 需要 make 命令执行。
在我们执行完 cmake .. 后,将生成 MakeFile 文件,然后执行 make 后便可以生成可执行文件。
这里我们进行补充说明:如果我们在 GNU/Linux 上,执行 CMake .. 后会生成 MakeFile 文件,然后执行 make 命令即可生成可执行文件;在 Windows 上,执行 cmake .. 后会生成 sln 文件,需要使用 VS 进行打开,然后对其进行生成操作。Windows 生成 sln 文件后的具体操作过程请参考最后一些补充内容。除此之外,我们可以执行以下命令,不分平台直接构建出可执行文件:
首先,如果对程序的生命周期的不清楚,请先移步这里进行学习。
在链接阶段,会将汇编生成的目标文件.o
与引用到的库一起链接打包到可执行文件中。这个链接方式为静态链接,所需要的.o(unix 系统)称为静态库。
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接成一个可执行文件。
- 静态库对程序的更新、部署和发布会带来麻烦。如果静态库 libxxx.o 更新了,所有使用它的应用程序都需要重新编译、发布给用户。
动态库在程序编译时并不会链接到目标代码中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存只需要有一份该共享库的实例,规避了空间浪费。
动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布带来的问题,用户只需要更新动态库即可,增量更新。
Windows 与 Linux 执行文件格式不同,在创建动态库的时候有一些差异:
- 在 Windows 系统下的执行文件格式是 PE 格式,动态库需要一个 DllMain 函数做初始化的入口,通常在导出函数的声明时需要有_declspec (dllexport) 关键字。
- Linux 下 gcc 编译的执行文件默认是 ELF 格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便
1
2
3
4
5
6
7
8
9
| .
├── message-module
│ ├── include
│ │ └── message.h
│ ├── src
│ │ └── message.cpp
│ └── CMakeLists.txt
├── hello_world.cpp
└── CMakeLists.txt
|
在实际的项目开发过程中,我们的项目结构往往会由很多个模块组成,每个模块通过一个单独的 CMakeLists.txt 去控制,最后在根目录下的 CMakeLists.txt 中将各个模块组合使用。
本项目中为了简化学习,只构建了一个 message-module 模块,构建多个模块的方式同理。其中项目中的所有 CPP 源文件与第一节内容相同,这里就不展开描述了。
源码地址:https://github.com/jianye0428/CMake_Learning_Notes/tree/main/Note_2/message_static_lib
1
2
3
4
5
6
7
8
9
10
11
12
13
| include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/include
)
file(GLOB HEADER ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
file(GLOB SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
add_library(
test_message
STATIC
${HEADER}
${SOURCE}
)
|
代码释义:
Tip
1
2
3
4
5
6
| add_library(
test_message
STATIC
${HEADER}
${SOURCE}
)
|
add_library
生成必要的构建指令,将指定的源码编译到库中。第一个参数是目标名。整个项目中,可使用相同的名称来引用库。生成的库的实际名称将由 CMake 通过在前面添加前缀 lib 和适当的扩展名作为后缀来形成。生成库是根据第二个参数 (STATIC 或 SHARED) 和操作系统确定的,本项目是将目标文件生成静态库。
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
|
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(hello-world LANGUAGES CXX)
# 设置可执行文件到bin文件夹下
set(EXECUTE_FILE ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTE_FILE})
# 设置静态库到lib文件夹下
set(LIB_FILE ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${LIB_FILE})
include_directories(
${CMAKE_SOURCE_DIR}/message-module/include
)
add_subdirectory(
${CMAKE_SOURCE_DIR}/message-module
)
add_executable(
${PROJECT_NAME}
${CMAKE_SOURCE_DIR}/hello_world.cpp
)
target_link_libraries(
${PROJECT_NAME}
test_message
)
|
代码释义:
Tip
1
2
3
4
5
6
7
| # 设置可执行文件到bin文件夹下
set(EXECUTE_FILE ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTE_FILE})
# 设置静态库到lib文件夹下
set(LIB_FILE ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${LIB_FILE})
|
在构建项目时,我们为了使得项目结构更加清晰,使得生成的可执行文件、静态库以及动态库等文件能够存放在合适的位置。这样的构建方式有助于我们在项目重构、项目优化、debug 的时候逻辑更加清晰。
Tip
1
2
3
| include_directories(
${CMAKE_SOURCE_DIR}/message-module/include
)
|
这个命令同第一节内容,因为 hello_world.cpp 要使用 message-module 模块的 API,且与该 CMakeLists.txt 在相同层级的目录,所以需要将 message-module 模块的 API 包含进去。
如果 hello_world.cpp 中使用到了多个模块,则此处可以包含多个模块的 API:
1
2
3
4
| include_directories(
${CMAKE_SOURCE_DIR}/message-module/include
${CMAKE_SOURCE_DIR}/xxx-module/include
)
|
Tip
1
2
3
| add_subdirectory(
${CMAKE_SOURCE_DIR}/message-module
)
|
将我们的 message-module 添加进来进行编译,这个函数命令将寻找 message-module 目录下的 CMakeLists.txt,如果该目录下没有 CMakeLists.txt 将报错。
由于在本项目中,hello_world.cpp 要使用 message-module 模块中编译生成的静态库,所以 add_subdirectory 命令将 message-module 添加到项目中,add_subdirectory 的顺序必须要先于 add_executable 命令。
Tip
1
2
3
4
5
6
7
8
9
| add_executable(
${PROJECT_NAME}
${CMAKE_SOURCE_DIR}/hello_world.cpp
)
target_link_libraries(
${PROJECT_NAME}
test_message
)
|
add_executable
命令将 hello_world.cpp
编译成可执行文件,其名字为项目名称 hello-world
,该可执行文件使用 target_link_libraries
命令将 message-module
模块下编译生成的静态库 test_message
链接到可执行文件中。
注意:在子模块 message-module 中编译生成的 test_message 是全局可见的,即任何模块或者根目录下的 CMakeLists.txt 都可以直接使用 test_message 进行调用。
1
2
3
| mkdir build
cd build
cmake ..
|
构建及编译过程:
1
2
3
4
5
6
7
8
9
10
| -- The CXX compiler identification is GNU 9.4.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/jiangli/repo/tutorials/cmake-tutorial/chapter1/03/build
|
1
2
| # cmake --build .
make
|
1
2
3
4
5
6
7
8
| Scanning dependencies of target test_message
[ 25%] Building CXX object message-module/CMakeFiles/test_message.dir/src/message.cpp.o
[ 50%] Linking CXX static library ../lib/libtest_message.a
[ 50%] Built target test_message
Scanning dependencies of target hello-world
[ 75%] Building CXX object CMakeFiles/hello-world.dir/hello_world.cpp.o
[100%] Linking CXX executable bin/hello-world
[100%] Built target hello-world
|
可以通过编译日志看到,首先编译了 message-module
模块,并将编译生成的 libtest_message.a
存档到了../lib/
,即 build 文件夹中的 lib 目录中。然后链接 hello-world
所需要的依赖项,此时便将 test-message
链接到了 hello-world
中,最终生成可执行文件 hello-world
,并将其存放到 bin
目录中,即 build 文件夹下的 bin 目录。
我们在构建实际项目过程中,一个项目往往需要链接许多的三方库,抑或是我们将自己的算法以静态库的形式发布,通常需要为我们的项目链接三方库。本节讲其中的一种,后续涉及到三方库的链接将讲述所有链接的方式。关于 third-party
模块下 include
文件夹下的 message.h
头文件与前面相同,lib 文件夹下的 libtest_message.a
是第三节编译生成的静态库。
1
2
3
4
5
6
7
8
| .
├── third-party
│ ├── include
│ │ └── message.h
│ └── lib
│ └── libtest_message.a
├── hello_world.cpp
└── CMakeLists.txt
|
一般,我们将三方库放到项目中一个 third-party
的文件夹下,当然你也可以随意命名。三方库 third-party
中包含 include
和 lib
分别存放三方库的 API 和静态库。
源码地址:https://github.com/jianye0428/CMake_Learning_Notes/tree/main/Note_2/message_static_lib_third_party
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.10 FATAL_ERROR)
project(hello-world LANGUAGES CXX)
# 设置可执行文件到bin文件夹下
set(EXECUTE_FILE ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTE_FILE})
set(TEST_MESSAGE ${CMAKE_SOURCE_DIR}/third-party/lib/libtest_message.a)
include_directories(
${CMAKE_SOURCE_DIR}/third-party/include
)
add_executable(
${PROJECT_NAME}
${CMAKE_SOURCE_DIR}/hello_world.cpp
)
target_link_libraries(
${PROJECT_NAME}
${TEST_MESSAGE}
)
|
代码释义:
Tip
1
| set(TEST_MESSAGE ${CMAKE_SOURCE_DIR}/third-party/lib/libtest_message.a)
|
将三方库中的静态库定义为 TEST_MESSAGE,方便后续使用 TESTMESSAGE 进行调用。当然你也可以直接在 targetlinklibraries 命令中使用 {CMAKE_SOURCE_DIR}/third-party/lib/libtest_message.a 进行链接,但是这么做是不推荐的。如果多个模块都使用到了该库,那么定义为宏的方式更加方便和清晰。
今后,我们都将定义出来的宏统一采用了大写,意和 CMake 自身变量命名对其。
1
2
3
| mkdir build
cd build
cmake ..
|
构建及编译过程:
1
2
3
4
5
6
7
8
9
10
| -- The CXX compiler identification is GNU 9.4.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/jiangli/repo/tutorials/cmake-tutorial/chapter1/04/build
|
1
2
3
4
| Scanning dependencies of target hello-world
[ 50%] Building CXX object CMakeFiles/hello-world.dir/hello_world.cpp.o
[100%] Linking CXX executable bin/hello-world
[100%] Built target hello-world
|
动态库的编写需要区分平台,在 GNU/Linux 平台上,动态库的编写和调用与静态库没有差别,但是在 Windows 平台上动态库的编写和调用需要做一定的修改。
在 GNU/Linux 上生成动态库的方法和静态库生成的方法类似,其目录结构等都与静态库相同,只有在使用 add_library
命令时,参数 STATIC
改为 SHARE
即可,相关项目结构和 CMakeLists.txt
如下。
1
2
3
4
5
6
7
8
9
| .
├── message-module
│ ├── include
│ │ └── message.h
│ ├── src
│ │ └── message.cpp
│ └── CMakeLists.txt
├── hello_world.cpp
└── CMakeLists.txt
|
源码地址: https://github.com/jianye0428/CMake_Learning_Notes/tree/main/Note_2/message_dynamic_lib
配置动态库的 CMakeLists.txt
:
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
| cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(hello-world LANGUAGES CXX)
# 设置可执行文件到bin文件夹下
set(EXECUTE_FILE ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTE_FILE})
# 设置动态库到lib文件夹下
set(LIB_FILE ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${LIB_FILE})
include_directories(
${CMAKE_SOURCE_DIR}/message-module/include
)
add_subdirectory(
${CMAKE_SOURCE_DIR}/message-module
)
add_executable(
${PROJECT_NAME}
${CMAKE_SOURCE_DIR}/hello_world.cpp
)
target_link_libraries(
${PROJECT_NAME}
test_message
)
|
这里我们设置动态库存放的路径的宏为 CMAKE_LIBRARY_OUTPUT_DIRECTORY。
源码地址:
在 GNU/Linux
上链接动态库的方法和静态库生成的方法类似,其目录结构等都与静态库相同,只有在使用 add_library 命令时,参数 STATIC 改为 SHARE 即可,相关项目结构和 CMakeLists.txt
如下。
目录结构:
1
2
3
4
5
6
7
8
| .
├── third-party
│ ├── include
│ │ └── message.h
│ └── lib
│ └── libtest_message.so
├── hello_world.cpp
└── CMakeLists.txt
|
源码地址: https://github.com/jianye0428/CMake_Learning_Notes/tree/main/Note_2/message_dynamic_lib_third_party
CMakeLists.txt
文件:
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.10 FATAL_ERROR)
project(hello-world LANGUAGES CXX)
# 设置可执行文件到bin文件夹下
set(EXECUTE_FILE ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTE_FILE})
set(TEST_MESSAGE ${CMAKE_SOURCE_DIR}/third-party/lib/libtest_message.so)
include_directories(
${CMAKE_SOURCE_DIR}/third-party/include
)
add_executable(
${PROJECT_NAME}
${CMAKE_SOURCE_DIR}/hello_world.cpp
)
target_link_libraries(
${PROJECT_NAME}
${TEST_MESSAGE}
)
|
注意:通过实践发现,Windows 中 CMAKE_LIBRARY_OUTPUT_DIRECTORY
没有作用。在 Windows 中生成的动态库将会自动生成到可执行文件所在的目录。
前面我们说 Windows 平台中生成动态库的源码和静态库是不同的,在 Windows 平台中,在导出动态库时除了会生成.dll
动态库之外,还会生成一个.lib
文件。注意,这个.lib
文件和静态库的.lib
文件是不同的,它里面并不保存代码生成的二进制文件,而是所有需要导出符号的符号表。因此这个.lib
文件和编译生成的静态库.lib
相比较而言会小的多。
符号表是需要我们在编写源码时进行指定的,如果我们将一个符号导出(符号可以指类、函数等各种类型), 需要在其前面加上__declspec(dllexport)
标志,这样这个符号的相关信息就会在导出的.lib
中的符号表中了。
如果在源码中没有任何的__declspec(dllexport)
, 依然可以成功的编译出动态库,但是并不会生成保存符号表的.lib
文件。
1
2
3
4
5
6
7
| class __declspec(dllexport) Message {
public:
Message() {}
void Print(const std::string &message);
};
|
除了导出符号标识符__declspec(dllexport)
以外,作为用户使用动态库的时候,对应的头文件的符号还需要__declspec(dllimport)
标识符来表示这个符号是从动态库导入的。
1
2
3
4
5
6
7
| class __declspec(dllimport) Message {
public:
Message() {}
void Print(const std::string &message);
};
|
一般,一个库文件我们并不想对导入和导出分别写两个几乎同样的头文件,因此可以使用宏定义来代替直接使用__declspec(dllexport)
和__declspec(dllimport)
关键字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#ifndef MESSAGE_HEADER_H_
#define MESSAGE_HEADER_H_
#ifdef SHARED_LIB_EXPORT
#define SHARED_LIB_EXPORT __declspec(dllexport)
#else
#define SHARED_LIB_EXPORT __declspec(dllimport)
#endif
class SHARED_LIB_EXPORT Message {
public:
Message() {}
void Print(const std::string &message);
};
#endif // ! MESSAGE_HEADER_H_
|
这样我们只需要在编译(导出)这个库的时候,给编译器添加 SHARED_LIB_EXPORT
宏。而在使用该库的时候什么都不定义即可。
我们通常编写一个头文件来专门管理 SHARED_LIB_EXPORT
宏定义。为了使得我们的代码在 Linux 中平台以及静态库的情况,我们的头文件编写如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #ifndef EXPORT_LIB_HEADER_H_
#define EXPORT_LIB_HEADER_H_
#ifdef SHARED_LIB_BUILD
#ifdef _WIN32
#ifdef SHARED_LIB_EXPORT
#define SHARED_LIB_API __declspec(dllexport)
#else
#define SHARED_LIB_API __declspec(dllimport)
#endif // SHARED_LIB_EXPORT
#else
#define SHARED_LIB_API
#endif // _WIN32
#else
#define SHARED_LIB_API
#endif // SHARED_LIB_BUILD
#endif // ! EXPORT_LIB_HEADER_H_
|
我们除了使用 SHARED_LIB_API
宏定义来判断是否导出为动态库以外,还使用了编译器自带的_WIN32
宏来判断是实在 windows 平台上以及使用。SHARED_LIB_BUILD
来判断是否正在编译动态库。
有了这个头文件之后,我们只需要在导出符号表的头文件中包含该头文件,就可以使用 SHARED_LIB_API
宏定义了。
除此之外,上述的头文件可以通过 CMake 提供的 GenerateExportHeader
命令自动生成。关于该命令的使用在后续介绍中会详细的进行探索。
项目结构:
1
2
3
4
5
6
7
8
9
| .
├── message-module
│ ├── include
│ │ └── message.h
│ ├── src
│ │ └── message.cpp
│ └── CMakeLists.txt
├── hello_world.cpp
└── CMakeLists.txt
|
源码地址:https://github.com/jianye0428/CMake_Learning_Notes/tree/main/Note_2/message_shared_lib_in_windows
message.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#ifndef MESSAGE_HEADER_H_
#define MESSAGE_HEADER_H_
#include <iostream>
#include <string>
class __declspec(dllexport) Message {
public:
Message() {}
void Print(const std::string &message);
};
#endif // ! MESSAGE_HEADER_H_
|
message.cpp
1
2
3
4
5
| #include "message.h"
void Message::Print(const std::string &message) {
std::cout << message << std::endl;
}
|
message-module 下的 CMakeLists.txt
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
| include_directories(
${CMAKE_SOURCE_DIR}/include
)
file(GLOB HEADER ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
file(GLOB SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
add_library(test_message SHARED ${SOURCE})
add_library(test_message::test_message ALIAS test_message)
target_include_directories(test_message
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>/include
$<INSTALL_INTERFACE:include>
)
set_target_properties(test_message PROPERTIES
CXX_STANDARD 11
CMAKE_CXX_STANDARD_REQUIRED True
)
install(TARGETS test_message
EXPORT message_export_target
RUNTIME DESTINATION "bin"
LIBRARY DESTINATION "lib"
ARCHIVE DESTINATION "lib")
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
DESTINATION "include"
FILES_MATCHING PATTERN "*.h")
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
DESTINATION "include"
FILES_MATCHING PATTERN "*.hpp")
|
代码释义:
Tip
1
| add_library(test_message::test_message ALIAS test_message)
|
添加别名,以便库可以在构建树中使用,例如在测试时。
Tip
1
2
3
4
5
| target_include_directories(test_message
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>/include
$<INSTALL_INTERFACE:include>
)
|
设置 test_message 的包含路径。其中,PUBLIC
表示这些头文件路径将会被暴露给该目标的依赖项,即其他目标可以通过依赖该目标来访问这些头文件路径。
BUILD_INTERFACE
表示在构建时使用的头文件路径;INSTALL_INTERFACE 表示在安装时使用的头文件路径,即将该目标安装到其他地方时,头文件将会被安装到 include 目录下。
综上,这段代码的作用是将当前项目的根目录添加到 “test_message” 目标的头文件包含路径中,以便在编译和安装时能够正确地访问这些头文件。
Tip
1
2
3
4
| set_target_properties(test_message PROPERTIES
CXX_STANDARD 11
CMAKE_CXX_STANDARD_REQUIRED True
)
|
设置 test_message
的相关属性,关于 target_include_directories
和 set_target_properties
的具体使用情况,我们将在以后做详细讲解。
Tip
1
2
3
4
5
| install(TARGETS test_message
EXPORT message_export_target
RUNTIME DESTINATION "bin"
LIBRARY DESTINATION "lib"
ARCHIVE DESTINATION "lib")
|
安装一个名为 “test_message” 的目标(在 Windows 上需要将编译出来的 sln 文件使用 vs 打开,然后再属性栏中的 INSTALL 右键生成,即可执行 install 命令)。其中,TARGETS 表示要安装的目标名称,即 test_message。
EXPORT message_export_target
表示将该目标导出到一个名为 “message_export_target” 的 CMake 配置文件中,以便其他项目可以使用该目标。
注意: 我们在根目录下已经指定了’install’安装目录的根目录。
RUNTIME DESTINATION ${CMAKE_SOURCE_DIR}/output/bin
表示将该目标的可执行文件安装到当前项目的 bin 目录下。
LIBRARY DESTINATION ${CMAKE_SOURCE_DIR}/output/lib
表示将该目标的共享库文件安装到 当前项目的 lib 目录下。
ARCHIVE DESTINATION ${CMAKE_SOURCE_DIR}/output/lib
) 表示将该目标的静态库文件安装到 当前项目的 lib 目录下。
注意: 在 Windows 构建动态库时,动态库 dll 会被安装到当前项目的 bin 目录下,目标文件.lib 文件会被安装到项目的 lib 目录下。
综上,这段代码的作用是将 “test_message” 目标的可执行文件、共享库文件和静态库文件安装到指定的目录下,并将该目标导出到一个 CMake 配置文件中,以便其他项目可以使用该目标。关于生成 cmake 相关配置文件详见下一篇内容。本项目仅用此命令做输出库的相关文件的功能。
Tip
1
2
3
4
5
6
| install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
DESTINATION ${CMAKE_SOURCE_DIR}/output/include
FILES_MATCHING PATTERN "*.h")
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
DESTINATION ${CMAKE_SOURCE_DIR}/output/include
FILES_MATCHING PATTERN "*.hpp")
|
将当前目录下的 include
目录中的.hpp
和.h
安装到当前项目下的 output
目录下的 include
文件中。关于 install 命令的具体使用方法,我们将以后做详细解释。
根目录下的 CMakeLists.txt
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
| cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(hello-world LANGUAGES CXX)
# 设置可执行文件到bin文件夹下
set(EXECUTE_FILE ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTE_FILE})
# 设置动态库到lib文件夹下
set(LIB_FILE ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${LIB_FILE})
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/output/)
endif()
add_subdirectory(
${CMAKE_SOURCE_DIR}/message-module
)
add_executable(
${PROJECT_NAME}
${CMAKE_SOURCE_DIR}/hello_world.cpp
)
target_link_libraries(
${PROJECT_NAME}
test_message
)
|
代码释义:
Tip
1
2
3
| if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/output/)
endif()
|
如果没有指定 install
的目录,则设置 install 的目录为 ${CMAKE_SOURCE_DIR}/output/
。
Windows 平台上链接动态库的使用方法和 GNU/Linux 平台上有所不同,我们在链接时需要将符号表文件 lib 进行连接,然后将对应的动态库文件 dll 文件拷贝到环境变量或者可执行文件所在的目录下,可知行文件才可以正常过执行。
项目结构
1
2
3
4
5
6
7
8
9
10
| .
├── third-party
│ ├── include
│ │ └── message.h
│ ├── lib
│ │ └── test_message.lib
│ └── bin
│ └── test_message.dll
├── hello_world.cpp
└── CMakeLists.txt
|
项目中使用的 third-party
使用的是上一节内容中在 Windows 上生成的动态库。文件 hello_world.cpp 与上一节内容相同,我们不再对其进行描述。
源码地址:https://github.com/jianye0428/CMake_Learning_Notes/tree/main/Note_2/message_shared_lib_third_party_in_windows
CMakeLists.txt
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
| cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(hello-world LANGUAGES CXX)
set(EXECUTE_FILE ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTE_FILE})
set(TEST_MESSAGE ${CMAKE_SOURCE_DIR}/third-party/lib/test_message.lib)
include_directories(
${CMAKE_SOURCE_DIR}/third-party/include
)
add_executable(
${PROJECT_NAME}
${CMAKE_SOURCE_DIR}/hello_world.cpp
)
target_link_libraries(
${PROJECT_NAME}
${TEST_MESSAGE}
)
if (MSVC)
file(GLOB MODEL "${CMAKE_SOURCE_DIR}/third-party/bin/*.dll")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${MODEL}
$<TARGET_FILE_DIR:${PROJECT_NAME}>)
endif()
|
代码释义:
Tip
1
| set(TEST_MESSAGE ${CMAKE_SOURCE_DIR}/third-party/lib/test_message.lib)
|
链接三方库的符号表
Tip
1
2
3
4
5
6
7
| if (MSVC)
file(GLOB MODEL "${CMAKE_SOURCE_DIR}/third-party/bin/*.dll")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${MODEL}
$<TARGET_FILE_DIR:${PROJECT_NAME}>)
endif()
|
将我们之前生成的动态库文件 dll 文件手动复制到可执行文件所在的目录中。关于 add_custom_command 用法我们之后章节将作详细详解。
在 window 上使用 vs 生成可执行文件和执行 install 命令。
首先,我们首先新建一个 build 目录,并进入该目录。
然后使用 cmake
进行构建项目。
然后在 build
目录下可以看到 sln
文件,使用 vs
打开。
构建所有: 在 vs 中的解决方案资源管理器中右键 ALL_BUILD,然后点击生成
编译生成 hello-world 进程,右键 hello-world, 然后点击生成。如果我们使用 CMake 在一个项目中生成了多个进程,我们在测试某一个进程时,在对应的进程上右键设为启动项目即可。
执行 install 安装命令: 在 INSTALL 上右键,然后点击生成即可。注意:只有当我们的 CMake 中有 install 命令时,VS 中才会出现 INTALL 选项。