CMake 秘籍(四)(3)https://developer.aliyun.com/article/1525213
工作原理
弃用函数或宏相当于重新定义它,如前一个示例所示,并打印带有 DEPRECATION
的消息:
macro(somemacro) message(DEPRECATION "somemacro is deprecated") _somemacro(${ARGV}) endmacro()
弃用变量可以通过首先定义以下内容来实现:
function(deprecate_variable _variable _access) if(_access STREQUAL "READ_ACCESS") message(DEPRECATION "variable ${_variable} is deprecated") endif() endfunction()
接下来,该函数将附加到即将被弃用的变量上:
variable_watch(somevariable deprecate_variable)
如果在这种情况下读取了 ${included_modules}
(READ_ACCESS
),则 deprecate_variable
函数会发出带有 DEPRECATION
的消息。
使用 add_subdirectory 限制作用域
本示例的代码可在 github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-07/recipe-07
获取,并包含一个 C++示例。该示例适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
在本章剩余的食谱中,我们将讨论项目结构化的策略,以及如何限制变量和副作用的范围,目的是降低代码复杂性并简化项目的维护。在本食谱中,我们将把一个项目拆分成多个CMakeLists.txt
文件,每个文件都有有限的范围,这些文件将使用add_subdirectory
命令进行处理。
准备工作
由于我们希望展示和讨论如何组织一个非平凡的项目,我们需要一个比“hello world”项目更复杂的示例。我们将开发一个相对简单的代码,它可以计算并打印基本细胞自动机:
en.wikipedia.org/wiki/Cellular_automaton#Elementary_cellular_automata
mathworld.wolfram.com/ElementaryCellularAutomaton.html
我们的代码将能够计算 256 种基本细胞自动机中的任何一种,例如规则 90(Wolfram 代码):
$ ./bin/automata 40 15 90 length: 40 number of steps: 15 rule: 90 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
我们示例代码项目的结构如下:
. ├── 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
中:
#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
之外的“外部”库提供的:
#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
文件在时间步长内传播系统:
#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
文件生成初始状态:
#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
文件包含打印一行的函数:
#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
文件解析命令行输入:
#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 库的两个单元测试:
#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 基础设施的详细解释:
- 顶层的
CMakeLists.txt
与食谱 1,使用函数和宏的代码重用非常相似:
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)
- 目标和源文件在
src/CMakeLists.txt
中定义(转换目标除外):
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 )
- 转换库在
external/CMakeLists.txt
中定义:
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} )
src/CMakeLists.txt
文件添加了更多的子目录,这些子目录又包含CMakeLists.txt
文件。它们的结构都类似;src/evolution/CMakeLists.txt
包含以下内容:
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} )
- 单元测试在
tests/CMakeLists.txt
中注册:
add_executable(cpp_test test.cpp) target_link_libraries(cpp_test evolution) add_test( NAME test_evolution COMMAND $<TARGET_FILE:cpp_test> )
- 配置和构建项目会产生以下输出:
$ 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
- 最后,我们运行单元测试:
$ 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
:
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
中访问它。将目标尽可能靠近代码定义的优点是,了解该库且可能对 CMake 框架知识有限的代码开发人员只需要编辑此目录中的文件;换句话说,库依赖关系被封装了。
向上移动一级,库在src/CMakeLists.txt
中组装:
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
,而是使用模块包含来组装不同的CMakeLists.txt
文件,这允许我们链接到当前目录之外定义的目标。
CMake 可以使用 Graphviz 图形可视化软件(www.graphviz.org
)来生成项目的依赖关系图:
$ cd build $ cmake --graphviz=example.dot .. $ dot -T png example.dot -o example.png
生成的图表将显示不同目录中目标之间的依赖关系:
在本书中,我们一直在进行源外构建,以保持源代码树和构建树分离。这是推荐的实践,允许我们使用相同的源代码配置不同的构建(顺序或并行,Debug
或Release
),而不需要复制源代码,也不需要在源代码树中散布生成的和对象文件。通过以下代码片段,您可以保护您的项目免受源内构建的影响:
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()
认识到构建树的结构模仿了源代码树的结构是很有用的。在我们的示例中,在src/CMakeLists.txt
中插入以下message
打印输出是相当有教育意义的:
message("current binary dir is ${CMAKE_CURRENT_BINARY_DIR}")
在配置项目以进行build
时,我们会看到打印输出指向build/src
。
另请参见
我们注意到,从 CMake 3.12 版本开始,OBJECT
库是组织大型项目的另一种可行方法。我们对示例的唯一修改将是在库的CMakeLists.txt
文件中。源代码将被编译成对象文件:既不会被归档到静态归档中,也不会被链接到共享库中。例如:
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
可执行目标将这些对象文件链接到最终的可执行文件中。使用要求,如包含目录、编译标志和链接库设置在OBJECT
库上将正确继承。有关 CMake 3.12 中引入的OBJECT
库新功能的更多详细信息,请参阅官方文档:cmake.org/cmake/help/v3.12/manual/cmake-buildsystem.7.html#object-libraries
使用 target_sources 避免全局变量
本配方的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-07/recipe-08
找到,并包含一个 C++示例。本配方适用于 CMake 3.5(及以上)版本,并在 GNU/Linux、macOS 和 Windows 上进行了测试。
在本配方中,我们将讨论与前一个配方不同的方法,并使用模块包含而不是使用add_subdirectory
来组装不同的CMakeLists.txt
文件。这种方法受到crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/
的启发,允许我们使用target_link_libraries
链接到当前目录之外定义的目标。
准备工作
我们将使用与之前配方相同的源代码。唯一的变化将在CMakeLists.txt
文件中,我们将在接下来的章节中讨论这些变化。
如何操作
让我们详细看看 CMake 所需的各个文件:
- 顶层
CMakeLists.txt
包含以下内容:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-08 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 include(src/CMakeLists.txt) include(external/CMakeLists.txt) enable_testing() add_subdirectory(tests)
external/CMakeLists.txt
文件与之前的配方相比没有变化。src/CMakeLists.txt
文件定义了两个库(automaton
和evolution
):
add_library(automaton "") add_library(evolution "") include(${CMAKE_CURRENT_LIST_DIR}/evolution/CMakeLists.txt) include(${CMAKE_CURRENT_LIST_DIR}/initial/CMakeLists.txt) include(${CMAKE_CURRENT_LIST_DIR}/io/CMakeLists.txt) include(${CMAKE_CURRENT_LIST_DIR}/parser/CMakeLists.txt) add_executable(automata "") target_sources(automata PRIVATE ${CMAKE_CURRENT_LIST_DIR}/main.cpp ) target_link_libraries(automata PRIVATE automaton conversion )
src/evolution/CMakeLists.txt
文件包含以下内容:
target_sources(automaton PRIVATE ${CMAKE_CURRENT_LIST_DIR}/evolution.cpp
PUBLIC ${CMAKE_CURRENT_LIST_DIR}/evolution.hpp ) target_include_directories(automaton PUBLIC ${CMAKE_CURRENT_LIST_DIR} ) target_sources(evolution PRIVATE ${CMAKE_CURRENT_LIST_DIR}/evolution.cpp PUBLIC ${CMAKE_CURRENT_LIST_DIR}/evolution.hpp ) target_include_directories(evolution PUBLIC ${CMAKE_CURRENT_LIST_DIR} )
- 剩余的
CMakeLists.txt
文件与src/initial/CMakeLists.txt
相同:
target_sources(automaton PRIVATE ${CMAKE_CURRENT_LIST_DIR}/initial.cpp PUBLIC ${CMAKE_CURRENT_LIST_DIR}/initial.hpp ) target_include_directories(automaton PUBLIC ${CMAKE_CURRENT_LIST_DIR} )
- 配置、构建和测试的结果与之前的配方相同:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build build $ 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
它是如何工作的
与之前的配方不同,我们定义了三个库:
conversion
(在external
中定义)automaton
(包含除转换之外的所有源文件)evolution
(在src/evolution
中定义,并由cpp_test
链接)
在这个例子中,我们通过使用include()
引用CMakeLists.txt
文件来保持父作用域中所有目标的可用性:
include(src/CMakeLists.txt) include(external/CMakeLists.txt)
我们可以构建一个包含树,记住当我们进入子目录(src/CMakeLists.txt
)时,我们需要使用相对于父作用域的路径:
include(${CMAKE_CURRENT_LIST_DIR}/evolution/CMakeLists.txt) include(${CMAKE_CURRENT_LIST_DIR}/initial/CMakeLists.txt) include(${CMAKE_CURRENT_LIST_DIR}/io/CMakeLists.txt) include(${CMAKE_CURRENT_LIST_DIR}/parser/CMakeLists.txt)
这样,我们可以在通过include()
语句访问的文件树中的任何地方定义和链接目标。然而,我们应该选择一个对维护者和代码贡献者来说最直观的地方来定义它们。
还有更多
我们可以再次使用 CMake 和 Graphviz(www.graphviz.org/
)来生成这个项目的依赖图:
$ cd build $ cmake --graphviz=example.dot .. $ dot -T png example.dot -o example.png
对于当前的设置,我们得到以下依赖图:
组织 Fortran 项目
本配方的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-07/recipe-09
找到,并包含一个 Fortran 示例。该配方适用于 CMake 版本 3.5(及更高版本),并在 GNU/Linux、macOS 和 Windows 上使用 MSYS Makefiles 进行了测试。
我们用一个配方来讨论如何结构化和组织 Fortran 项目,原因有二:
- 仍然有许多 Fortran 项目存在,特别是在数值软件领域(对于更全面的通用目的 Fortran 软件项目列表,请参见
fortranwiki.org/fortran/show/Libraries
)。 - Fortran 90(及以后版本)对于不使用 CMake 的项目来说,构建起来可能会更加困难,因为 Fortran 模块文件要求编译顺序。换句话说,对于手动编写的 Makefile,通常需要为 Fortran 模块文件编写一个依赖扫描器。
正如我们将在本配方中看到的,现代 CMake 允许我们以非常紧凑和模块化的方式表达配置和构建过程。作为一个例子,我们将使用前两个配方中的基本细胞自动机,现在移植到 Fortran。
CMake 秘籍(四)(5)https://developer.aliyun.com/article/1525217