# 7.7 add_subdirectory的限定范围
**NOTE**:*此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-7/recipe-07 中找到,其中有一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。*
本章剩下的示例中,我们将讨论构建项目的策略,并限制变量的范围和副作用,目的是降低代码的复杂性和简化项目的维护。这个示例中,我们将把一个项目分割成几个范围有限的CMakeLists.txt文件,这些文件将使用`add_subdirectory`命令进行处理。
## 准备工作
由于我们希望展示和讨论如何构造一个复杂的项目,所以需要一个比“hello world”项目更复杂的例子:
* https://en.wikipedia.org/wiki/Cellular_automaton#Elementary_cellular_automata
* http://mathworld.wolfram.com/ElementaryCellularAutomaton.html
我们的代码将能够计算任何256个基本细胞自动机,例如:规则90 (Wolfram代码):
![](https://img.kancloud.cn/14/ee/14ee352f4417122041ffef5ff3cb0a1b_670x745.png)
我们示例代码项目的结构如下:
```shell
.
├── CMakeLists.txt
├── external
│ ├── CMakeLists.txt
│ ├── conversion.cpp
│ ├── conversion.hpp
│ └── README.md
├── src
│ ├── CMakeLists.txt
│ ├── evolution
│ │ ├── CMakeLists.txt
│ │ ├── evolution.cpp
│ │ └── evolution.hpp
│ ├── initial
│ │ ├── CMakeLists.txt
│ │ ├── initial.cpp
│ │ └── initial.hpp
│ ├── io
│ │ ├── CMakeLists.txt
│ │ ├── io.cpp
│ │ └── io.hpp
│ ├── main.cpp
│ └── parser
│ ├── CMakeLists.txt
│ ├── parser.cpp
│ └── parser.hpp
└── tests
├── catch.hpp
├── CMakeLists.txt
└── test.cpp
```
我们将代码分成许多库来模拟真实的大中型项目,可以将源代码组织到库中,然后将库链接到可执行文件中。
主要功能在`src/main.cpp`中:
```c++
#include "conversion.hpp"
#include "evolution.hpp"
#include "initial.hpp"
#include "io.hpp"
#include "parser.hpp"
#include <iostream>
int main(int argc, char *argv[]) {
// parse arguments
int length, num_steps, rule_decimal;
std::tie(length, num_steps, rule_decimal) = parse_arguments(argc, argv);
// print information about parameters
std::cout << "length: " << length << std::endl;
std::cout << "number of steps: " << num_steps << std::endl;
std::cout << "rule: " << rule_decimal << std::endl;
// obtain binary representation for the rule
std::string rule_binary = binary_representation(rule_decimal);
// create initial distribution
std::vector<int> row = initial_distribution(length);
// print initial configuration
print_row(row);
// the system evolves, print each step
for (int step = 0; step < num_steps; step++) {
row = evolve(row, rule_binary);
print_row(row);
}
}
```
`external/conversion.cpp`文件包含要从十进制转换为二进制的代码。
我们在这里模拟这段代码是由`src`外部的“外部”库提供的:
```cmake
#include "conversion.hpp"
#include <bitset>
#include <string>
std::string binary_representation(const int decimal) {
return std::bitset<8>(decimal).to_string();
}
```
`src/evolution/evolution.cpp`文件为一个时限传播系统:
```c++
#include "evolution.hpp"
#include <string>
#include <vector>
std::vector<int> evolve(const std::vector<int> row, const std::string rule_binary) {
std::vector<int> result;
for (auto i = 0; i < row.size(); ++i) {
auto left = (i == 0 ? row.size() : i) - 1;
auto center = i;
auto right = (i + 1) % row.size();
auto ancestors = 4 * row[left] + 2 * row[center] + 1 * row[right];
ancestors = 7 - ancestors;
auto new_state = std::stoi(rule_binary.substr(ancestors, 1));
result.push_back(new_state);
}
return result;
}
```
`src/initial/initial.cpp`文件,对出进行初始化:
```cmake
#include "initial.hpp"
#include <vector>
std::vector<int> initial_distribution(const int length) {
// we start with a vector which is zeroed out
std::vector<int> result(length, 0);
// more or less in the middle we place a living cell
result[length / 2] = 1;
return result;
}
```
`src/io/io.cpp`文件包含一个函数输出打印行:
```c++
#include "io.hpp"
#include <algorithm>
#include <iostream>
#include <vector>
void print_row(const std::vector<int> row) {
std::for_each(row.begin(), row.end(), [](int const &value) {
std::cout << (value == 1 ? '*' : ' ');
});
std::cout << std::endl;
}
```
`src/parser/parser.cpp`文件解析命令行输入:
```c++
#include "parser.hpp"
#include <cassert>
#include <string>
#include <tuple>
std::tuple<int, int, int> parse_arguments(int argc, char *argv[]) {
assert(argc == 4 && "program called with wrong number of arguments");
auto length = std::stoi(argv[1]);
auto num_steps = std::stoi(argv[2]);
auto rule_decimal = std::stoi(argv[3]);
return std::make_tuple(length, num_steps, rule_decimal);
}
```
最后,`tests/test.cpp`包含两个使用Catch2库的单元测试:
```c++
#include "evolution.hpp"
// this tells catch to provide a main()
// only do this in one cpp file
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
#include <string>
#include <vector>
TEST_CASE("Apply rule 90", "[rule-90]") {
std::vector<int> row = {0, 1, 0, 1, 0, 1, 0, 1, 0};
std::string rule = "01011010";
std::vector<int> expected_result = {1, 0, 0, 0, 0, 0, 0, 0, 1};
REQUIRE(evolve(row, rule) == expected_result);
}
TEST_CASE("Apply rule 222", "[rule-222]") {
std::vector<int> row = {0, 0, 0, 0, 1, 0, 0, 0, 0};
std::string rule = "11011110";
std::vector<int> expected_result = {0, 0, 0, 1, 1, 1, 0, 0, 0};
REQUIRE(evolve(row, rule) == expected_result);
}
```
相应的头文件包含函数声明。有人可能会说,对于这个小代码示例,项目包含了太多子目录。请注意,这只是一个项目的简化示例,通常包含每个库的许多源文件,理想情况下,这些文件被放在到单独的目录中。
## 具体实施
让我们来详细解释一下CMake所需的功能:
1. `CMakeLists.txt`顶部非常类似于第1节,代码重用与函数和宏:
```cmake
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-07 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
# defines targets and sources
add_subdirectory(src)
# contains an "external" library we will link to
add_subdirectory(external)
# enable testing and define tests
enable_testing()
add_subdirectory(tests)
```
2. 目标和源在`src/CMakeLists.txt`中定义(转换目标除外):
```cmake
add_executable(automata main.cpp)
add_subdirectory(evolution)
add_subdirectory(initial)
add_subdirectory(io)
add_subdirectory(parser)
target_link_libraries(automata
PRIVATE
conversion
evolution
initial
io
parser
)
```
3. 转换库在`external/CMakeLists.txt`中定义:
```cmake
add_library(conversion "")
target_sources(conversion
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/conversion.cpp
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/conversion.hpp
)
target_include_directories(conversion
PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)
```
4. `src/CMakeLists.txt`文件添加了更多的子目录,这些子目录又包含`CMakeLists.txt`文件。`src/evolution/CMakeLists.txt`包含以下内容:
```cmake
add_library(evolution "")
target_sources(evolution
PRIVATE
evolution.cpp
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/evolution.hpp
)
target_include_directories(evolution
PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)
```
5. 单元测试在`tests/CMakeLists.txt`中注册:
```cmake
add_executable(cpp_test test.cpp)
target_link_libraries(cpp_test evolution)
add_test(
NAME
test_evolution
COMMAND
$<TARGET_FILE:cpp_test>
)
```
6. 配置和构建项目产生以下输出:
```shell
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
Scanning dependencies of target conversion
[ 7%] Building CXX object external/CMakeFiles/conversion.dir/conversion.cpp.o
[ 14%] Linking CXX static library ../lib64/libconversion.a
[ 14%] Built target conversion
Scanning dependencies of target evolution
[ 21%] Building CXX object src/evolution/CMakeFiles/evolution.dir/evolution.cpp.o
[ 28%] Linking CXX static library ../../lib64/libevolution.a
[ 28%] Built target evolution
Scanning dependencies of target initial
[ 35%] Building CXX object src/initial/CMakeFiles/initial.dir/initial.cpp.o
[ 42%] Linking CXX static library ../../lib64/libinitial.a
[ 42%] Built target initial
Scanning dependencies of target io
[ 50%] Building CXX object src/io/CMakeFiles/io.dir/io.cpp.o
[ 57%] Linking CXX static library ../../lib64/libio.a
[ 57%] Built target io
Scanning dependencies of target parser
[ 64%] Building CXX object src/parser/CMakeFiles/parser.dir/parser.cpp.o
[ 71%] Linking CXX static library ../../lib64/libparser.a
[ 71%] Built target parser
Scanning dependencies of target automata
[ 78%] Building CXX object src/CMakeFiles/automata.dir/main.cpp.o
[ 85%] Linking CXX executable ../bin/automata
[ 85%] Built target automata
Scanning dependencies of target cpp_test
[ 92%] Building CXX object tests/CMakeFiles/cpp_test.dir/test.cpp.o
[100%] Linking CXX executable ../bin/cpp_test
[100%] Built target cpp_test
```
7. 最后,运行单元测试:
```shell
$ ctest
Running tests...
Start 1: test_evolution
1/1 Test #1: test_evolution ................... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
```
## 工作原理
我们可以将所有代码放到一个源文件中。不过,每次编辑都需要重新编译。将源文件分割成更小、更易于管理的单元是有意义的。可以将所有源代码都编译成一个库或可执行文件。实际上,项目更喜欢将源代码编译分成更小的、定义良好的库。这样做既是为了本地化和简化依赖项,也是为了简化代码维护。这意味着如在这里所做的那样,由许多库构建一个项目是一种常见的情况。
为了讨论CMake结构,我们可以从定义每个库的单个CMakeLists.txt文件开始,自底向上进行,例如`src/evolution/CMakeLists.txt`:
```cmake
add_library(evolution "")
target_sources(evolution
PRIVATE
evolution.cpp
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/evolution.hpp
)
target_include_directories(evolution
PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)
```
这些单独的`CMakeLists.txt`文件定义了库。本例中,我们首先使用`add_library`定义库名,然后定义它的源和包含目录,以及它们的目标可见性:实现文件(`evolution.cpp`:`PRIVATE`),而接口头文件` evolution.hpp `定义为`PUBLIC`,因为我们将在`main.cpp`和`test.cpp`中访问它。定义尽可能接近代码目标的好处是,对于该库的修改,只需要变更该目录中的文件即可;换句话说,也就是库依赖项被封装。
向上移动一层,库在`src/CMakeLists.txt`中封装:
```cmake
add_executable(automata main.cpp)
add_subdirectory(evolution)
add_subdirectory(initial)
add_subdirectory(io)
add_subdirectory(parser)
target_link_libraries(automata
PRIVATE
conversion
evolution
initial
io
parser
)
```
文件在主`CMakeLists.txt`中被引用。这意味着使用`CMakeLists.txt`文件,构建我们的项目。这种方法对于许多项目来说是可用的,并且它可以扩展到更大型的项目,而不需要在目录间的全局变量中包含源文件列表。`add_subdirectory`方法的另一个好处是它隔离了作用范围,因为子目录中定义的变量在父范围中不能访问。
## 更多信息
使用`add_subdirectory`调用树构建项目的一个限制是,CMake不允许将`target_link_libraries`与定义在当前目录范围之外的目标一起使用。对于本示例来说,这不是问题。在下一个示例中,我们将演示另一种方法,我们不使用`add_subdirectory`,而是使用`module include`来组装不同的`CMakeLists.txt`文件,它允许我们链接到当前目录之外定义的目标。
CMake可以使用Graphviz图形可视化软件(http://www.graphviz.org )生成项目的依赖关系图:
```shell
$ cd build
$ cmake --graphviz=example.dot ..
$ dot -T png example.dot -o example.png
```
生成的图表将显示不同目录下的目标之间的依赖关系:
![](https://img.kancloud.cn/48/8c/488c03180e9f75188bcef010e25de00f_2480x525.png)
本书中,我们一直在构建源代码之外的代码,以保持源代码树和构建树是分开的。这是推荐的方式,允许我们使用相同的源代码配置不同的构建(顺序的或并行的,Debug或Release),而不需要复制源代码,也不需要在源代码树中生成目标文件。使用以下代码片段,可以保护您的项目免受内部构建的影响:
```cmake
if(${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR})
message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there.")
endif()
```
认识到构建结构与源结构类似很有用。示例中,将`message`打印输出插入到`src/CMakeLists.txt`中:
```cmake
message("current binary dir is ${CMAKE_CURRENT_BINARY_DIR}")
```
在`build`下构建项目时,我们将看到`build/src`的打印输出。
在CMake的3.12版本中,`OBJECT`库是组织大型项目的另一种可行方法。对我们的示例的惟一修改是在库的`CMakeLists.tx`t中。源文件将被编译成目标文件:既不存档到静态库中,也不链接到动态库中。例如:
```cmake
add_library(io OBJECT "")
target_sources(io
PRIVATE
io.cpp
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/io.hpp
)
target_include_directories(io
PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)
```
主`CMakeLists.txt`保持不变:`automata`可执行目标将这些目标文件链接到最终的可执行文件。使用也有要求需求,例如:在对象库上设置的目录、编译标志和链接库,将被正确地继承。有关CMake 3.12中引入的对象库新特性的更多细节,请参考官方文档: https://cmake.org/cmake/help/v3.12/manual/cmake-buildsystem.7.html#object-libraries
- Introduction
- 前言
- 第0章 配置环境
- 0.1 获取代码
- 0.2 Docker镜像
- 0.3 安装必要的软件
- 0.4 测试环境
- 0.5 上报问题并提出改进建议
- 第1章 从可执行文件到库
- 1.1 将单个源文件编译为可执行文件
- 1.2 切换生成器
- 1.3 构建和链接静态库和动态库
- 1.4 用条件句控制编译
- 1.5 向用户显示选项
- 1.6 指定编译器
- 1.7 切换构建类型
- 1.8 设置编译器选项
- 1.9 为语言设定标准
- 1.10 使用控制流
- 第2章 检测环境
- 2.1 检测操作系统
- 2.2 处理与平台相关的源代码
- 2.3 处理与编译器相关的源代码
- 2.4 检测处理器体系结构
- 2.5 检测处理器指令集
- 2.6 为Eigen库使能向量化
- 第3章 检测外部库和程序
- 3.1 检测Python解释器
- 3.2 检测Python库
- 3.3 检测Python模块和包
- 3.4 检测BLAS和LAPACK数学库
- 3.5 检测OpenMP的并行环境
- 3.6 检测MPI的并行环境
- 3.7 检测Eigen库
- 3.8 检测Boost库
- 3.9 检测外部库:Ⅰ. 使用pkg-config
- 3.10 检测外部库:Ⅱ. 自定义find模块
- 第4章 创建和运行测试
- 4.1 创建一个简单的单元测试
- 4.2 使用Catch2库进行单元测试
- 4.3 使用Google Test库进行单元测试
- 4.4 使用Boost Test进行单元测试
- 4.5 使用动态分析来检测内存缺陷
- 4.6 预期测试失败
- 4.7 使用超时测试运行时间过长的测试
- 4.8 并行测试
- 4.9 运行测试子集
- 4.10 使用测试固件
- 第5章 配置时和构建时的操作
- 5.1 使用平台无关的文件操作
- 5.2 配置时运行自定义命令
- 5.3 构建时运行自定义命令:Ⅰ. 使用add_custom_command
- 5.4 构建时运行自定义命令:Ⅱ. 使用add_custom_target
- 5.5 构建时为特定目标运行自定义命令
- 5.6 探究编译和链接命令
- 5.7 探究编译器标志命令
- 5.8 探究可执行命令
- 5.9 使用生成器表达式微调配置和编译
- 第6章 生成源码
- 6.1 配置时生成源码
- 6.2 使用Python在配置时生成源码
- 6.3 构建时使用Python生成源码
- 6.4 记录项目版本信息以便报告
- 6.5 从文件中记录项目版本
- 6.6 配置时记录Git Hash值
- 6.7 构建时记录Git Hash值
- 第7章 构建项目
- 7.1 使用函数和宏重用代码
- 7.2 将CMake源代码分成模块
- 7.3 编写函数来测试和设置编译器标志
- 7.4 用指定参数定义函数或宏
- 7.5 重新定义函数和宏
- 7.6 使用废弃函数、宏和变量
- 7.7 add_subdirectory的限定范围
- 7.8 使用target_sources避免全局变量
- 7.9 组织Fortran项目
- 第8章 超级构建模式
- 8.1 使用超级构建模式
- 8.2 使用超级构建管理依赖项:Ⅰ.Boost库
- 8.3 使用超级构建管理依赖项:Ⅱ.FFTW库
- 8.4 使用超级构建管理依赖项:Ⅲ.Google Test框架
- 8.5 使用超级构建支持项目
- 第9章 语言混合项目
- 9.1 使用C/C++库构建Fortran项目
- 9.2 使用Fortran库构建C/C++项目
- 9.3 使用Cython构建C++和Python项目
- 9.4 使用Boost.Python构建C++和Python项目
- 9.5 使用pybind11构建C++和Python项目
- 9.6 使用Python CFFI混合C,C++,Fortran和Python
- 第10章 编写安装程序
- 10.1 安装项目
- 10.2 生成输出头文件
- 10.3 输出目标
- 10.4 安装超级构建
- 第11章 打包项目
- 11.1 生成源代码和二进制包
- 11.2 通过PyPI发布使用CMake/pybind11构建的C++/Python项目
- 11.3 通过PyPI发布使用CMake/CFFI构建C/Fortran/Python项目
- 11.4 以Conda包的形式发布一个简单的项目
- 11.5 将Conda包作为依赖项发布给项目
- 第12章 构建文档
- 12.1 使用Doxygen构建文档
- 12.2 使用Sphinx构建文档
- 12.3 结合Doxygen和Sphinx
- 第13章 选择生成器和交叉编译
- 13.1 使用CMake构建Visual Studio 2017项目
- 13.2 交叉编译hello world示例
- 13.3 使用OpenMP并行化交叉编译Windows二进制文件
- 第14章 测试面板
- 14.1 将测试部署到CDash
- 14.2 CDash显示测试覆盖率
- 14.3 使用AddressSanifier向CDash报告内存缺陷
- 14.4 使用ThreadSaniiser向CDash报告数据争用
- 第15章 使用CMake构建已有项目
- 15.1 如何开始迁移项目
- 15.2 生成文件并编写平台检查
- 15.3 检测所需的链接和依赖关系
- 15.4 复制编译标志
- 15.5 移植测试
- 15.6 移植安装目标
- 15.7 进一步迁移的措施
- 15.8 项目转换为CMake的常见问题
- 第16章 可能感兴趣的书
- 16.1 留下评论——让其他读者知道你的想法