CMake Cookbook精要

CMake允许您描述如何在所有主要硬件和操作系统上配置、构建和安装项目,无论是构建可执行文件、库还是两者。
CTest允许您定义测试、测试套件,并设置它们的执行方式。
CPack为您的所有打包需求提供了一个DSL,不管您的项目是以源代码还是平台本机二进制形式捆绑和分发的。
CDash对于向在线仪表板报告项目的测试结果非常有用。

您可以在这里下载CMake Cookbook的随书示例源代码:
https://github.com/dev-cafe/cmake-cookbook

其他阅读资源
在线提供的CMake文档是全面的,我们将在本书中查阅:https://cmake.org/documentation/
在编写这本书时,我们也受到了其他资源的启发:
Daniel Pfeifer的演讲,可在GitHub上获得:https://github.com/bootcon/cppnow_presentations_2017/blob/master/05-19-2017_Friday/effective_cmake_uuu daniel_Pfeifer_cppnow_05-19-2017.pdf
Eric Noulard的CMake教程,可在GitHub上获得:https://github.com/theerk/cmake-tutorial
Craig Scott的CMake相关博客文章:https://crascit.com/tag/cmake/
我们还可以推荐浏览CMake资源、脚本和模块和示例由Viktor Kirilov收集:https://github.com/onqtam/awesome-cmake
同样值得注意的是,我们的书并不是唯一一本涉及到CMake的书:
《Mastering CMake》,Ken Martin和Bill Hoffman著,2015年,Kitware Inc.
《Professional CMake》,Craig Scott著:https://crascit.com/professional-cmake/

安装必备软件
1、CMake
2、特定于语言的工具,即编译器
3、构建自动化工具
4、Python

获得CMake
CMake 3.5是本书需要的最低版本。只有少数特定的方案和示例演示了在3.5版之后引入的有用功能,它们将需要较新版本的CMake。每个方案的介绍都有一个信息框,指出代码在哪里可用,给出了哪些示例,以及所需的CMake的最低版本。大多数GNU/Linux发行版在包管理器中提供了CMake。

编译器
本书需要C/C++/Fortran编译器。
在GNU/Linux上,GCC是理所当然的选择,它是自由软件且在所有发行版中都有提供。举个例子,在Ubuntu上,您可以用以下命令安装这些编译器:
sudo apt-get install gcc g++ gfortran
LLVM家族中的Clang编译器,对于C/C++也是相当好的选择:
sudo apt-get install clang clang++ gfortran

构建自动化工具
在GNU/Linux上,安装编译器时,GNU Make很可能会自动安装。
Ninja程序是一个不同的构建自动化工具,可以在GNU/Linux、MacOS和Windows上工作。Ninja是一种新的构建工具,专注于速度,尤其是增量重建。GNU/Linux、MacOS和Windows的预打包二进制文件可以在项目的GitHub存储库中找到,网址为:https://github.com/ninja-build/ninja/releases
在Ubuntu上,您可以用以下命令安装Ninja程序:
sudo apt-get install ninja-build

Python
Python 2.7将于2020年结束支持,所以我们将使用Python 3.6。
在Ubuntu 18.04 LTS上,您可以用以下命令安装Python 3.6:
sudo apt-get install python3-dev

为了运行我们已经安装好的测试机器,还需要特定的Python模块。这些可以通过使用您最喜欢的包管理器在系统范围内安装,也可以在独立的环境中安装。强烈建议采用后一种方法,因为它具有以下优点:
您可以在不影响系统环境的情况下安装软件包并清理安装。
可以在没有管理员权限的情况下安装包。
您可以降低版本和依赖关系冲突的风险。
为了再现性,您可以更好地控制包依赖性。
为此,我们准备了一个pipfile。结合pipfile.lock,您可以使用pipenv(http://pipenv.readthedocs.io)生成一个独立的环境,并安装所有包。要为方案示例存储库创建此环境,请在存储库的顶级目录中运行以下命令:
pip3 install --user pip pipenv --upgrade
pipenv install --python python3

附加软件
BLAS和LAPACK
大多数Linux发行版提供了BLAS和LAPACK软件包。例如,在Ubuntu 18.04 LTS上,您可以用以下命令安装它们:
sudo apt-get install libatlas-base-dev liblapack-dev liblapacke-dev

消息传递接口(MPI)
MPI有许多商业和非商业的实现。出于介绍的目的,安装任何免费可用的非商业实现都是足够的。在Ubuntu 18.04 LTS上,我们推荐OpenMPI,您可以用以下命令安装它:
sudo apt-get install openmpi-bin libopenmpi-dev

Boost程序库
每个操作系统都有Boost软件包。大多数Linux发行版都通过其包管理器提供此软件包。在Ubuntu 18.04 LTS上,您可以用以下命令安装Boost Filesystem、Boost Python和Boost程序库:
sudo apt-get install libboost-filesystem-dev libboost-python-dev libboost-test-dev

交叉编译器
在Debian/Ubuntu-like系统上,可以用以下命令安装交叉编译器:
sudo apt-get install gcc-mingw-w64 g++-mingw-w64 gfortran-mingw-w64

ZeroMQ、pkg-config、UUID和Doxygen
在Ubuntu 18.04 LTS上,可以用以下命令安装这些软件包:
sudo apt-get install pkg-config libzmq3-dev doxygen graphviz-dev uuid-dev

测试方案
在测试方案之前,用以下命令安装一些模块:
pipenv install --skip-lock colorama yml
以下命令将测试第1章和第7章中的Recipe1、2和5:
cd cmake-cookbook-1.0
pipenv run python testing/collect_tests.py 'chapter-0[1,7]/recipe-0[1,2,5]'
要获得更详细的输出,设置“VERBOSE_OUTPUT=ON”:
env VERBOSE_OUTPUT=ON pipenv run python testing/collect_tests.py 'chapter-0[1,7]/recipe-0[1,2,5]'

从简单的可执行文件到库
在本章中,我们将介绍以下方案:
将单个源文件编译为可执行文件
切换生成器
建立和链接静态和共享库
用条件控制编译
向用户演示选项
指定编译器
切换生成类型
控制编译器标志
设置语言标准
使用控制流构造

编译单个源文件为可执行程序
我们希望编译以下源代码为一个可执行程序:

#include
#include
#include
std::string say_hello() { return std::string("Hello, CMake world!"); }
int main() {
    std::cout << say_hello() << std::endl;
    return EXIT_SUCCESS;
}

除了源文件之外,我们还需要向CMake提供一个要执行的操作的描述,以便为构建工具配置项目。描述以CMake语言完成,其综合文档可在https://cmake.org/cmake/help/latest/在线找到。我们将把CMake指令放入一个名为CMakeLists.txt的文件中。

具体步骤如下:
1、用您最喜欢的编辑器打开文本文件。此文件的名称将为CMakeLists.txt。
2、以下第一行为CMake设置了所需的最低版本。如果使用的CMake版本低于该版本,将发出致命错误:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
3、第二行声明项目名称(recipe-01)和支持的语言(CXX代表C++):
project(recipe-01 LANGUAGES CXX)
4、我们指示CMake创建一个新的目标:可执行程序hello-world。此可执行文件是通过编译和链接源文件hello-world.cpp生成的。CMake将使用所选编译器和生成自动化工具的默认设置:
add_executable(hello-world hello-world.cpp)
5、将文件保存在与源文件hello-world.cpp相同的目录中。请记住,它只能命名为CMakeLists.txt。
6、现在,我们可以通过创建和单步执行构建目录来配置项目:
mkdir -p build
cd build
cmake ..
7、如果一切顺利,那么项目的配置已经在build目录中生成。我们现在可以编译可执行文件:
cmake --build .
在此方案中,我们使用了一个CMakeLists.txt文件来构建“Hello world”可执行程序:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-01 LANGUAGES CXX)
add_executable(hello-world hello-world.cpp)
注意:CMake语言是大小写不敏感的,但参数是大小写敏感的。在CMake中,C++是默认编程语言。但是,我们建议始终使用LANGUAGE选项来显式地声明项目使用的语言。
为生成生成生成系统而发出的典型命令系列如下:
mkdir -p build
cd build
cmake ..
在此,我们创建了一个目录build,在其中生成构建系统,我们进入了build目录,并通过将其指向CMakeLists.txt的位置(在本例中位于父目录)来调用CMake。也可以使用以下调用来实现相同的效果:
cmake -H. -Bbuild
此调用是跨平台的,且引入了-H和-B命令行界面开关。我们使用-H.来指示CMake在当前目录去找CMakeLists.txt文件。-Bbuild告诉CMake在一个名为build的目录中生成它的所有文件。

CMake是一个构建系统生成器。它支持Unix Makefile、Ninja、Visual Studio甚至更多的构建系统。在GNU/Linux和MacOS上,CMake使用Unix Makefile。在Windows上,Visual Studio是默认的生成器。我们将更详细地了解下一个方案中的生成器,并在第13章“替代生成器和交叉编译”中重新讨论生成器。

在GNU/Linux上,CMake默认生成Unix Makefile以构建项目:
1、Makefile:生成的指令集将运行以生成(make)项目。
2、CMakeFiles:包含临时文件的目录,CMake用于检测操作系统、编译器等。此外,根据所选的生成器,它还包含特定于项目的文件。
3、cmake_install.cmake:处理安装规则的CMake脚本,在安装时使用。
4、CMakeCache.txt:如文件名所示,CMake缓存。CMake在重新运行配置时使用此文件。

使用CMake的常见步骤:
mkdir -p /tmp/someplace
cd /tmp/someplace
cmake /path/to/source
cmake --build .

hello-world可执行程序是我们要生成的唯一目标,但运行以下命令:
cmake --build . --target help
可以看到CMake生成的目标比构建可执行程序需要的目标多。这些目标可以用cmake --build . --target 语法和实现来选择:
all(或Visual Studio生成器使用ALL_BUILD)是默认目标且会构建项目中的全部其它目标。
clean用来删除生成的全部文件。
depend会调用CMake为源文件生成依赖项(如果有)。
rebuild_cache将再次调用CMake来重建CMakeCache.txt。如果需要添加源中的新项,则需要这样做。
edit_cache此目标将允许您直接编辑缓存项。
对于更复杂的项目,通过测试阶段和安装规则,CMake将生成额外的便利目标:
test(或Visual Studio生成器中的RUN_TESTS)将在CTest的帮助下运行测试套件。我们将在第4章“创建和运行测试”中广泛讨论测试和CTest。
install会执行项目的安装规则。我们将在第10章“编写安装程序”中讨论安装规则。
package这个目标会调用CPack为项目生成可再发行的包。我们将在第11章”打包项目“中讨论打包和CPack。

切换生成器
您可以用以下命令查看当前安装的CMake支持的生成器:
cmake --help
Ubuntu 18.04 LTS/CMake 3.10支持的生成器:
Unix Makefiles = Generates standard UNIX makefiles.
Ninja = Generates build.ninja files.
Watcom WMake = Generates Watcom WMake makefiles.
CodeBlocks - Ninja = Generates CodeBlocks project files.
CodeBlocks - Unix Makefiles = Generates CodeBlocks project files.
CodeLite - Ninja = Generates CodeLite project files.
CodeLite - Unix Makefiles = Generates CodeLite project files.
Sublime Text 2 - Ninja = Generates Sublime Text 2 project files.
Sublime Text 2 - Unix Makefiles = Generates Sublime Text 2 project files.
Kate - Ninja = Generates Kate project files.
Kate - Unix Makefiles = Generates Kate project files.
Eclipse CDT4 - Ninja = Generates Eclipse CDT 4.0 project files.
Eclipse CDT4 - Unix Makefiles = Generates Eclipse CDT 4.0 project files.
KDevelop3 = Generates KDevelop 3 project files.
KDevelop3 - Unix Makefiles = Generates KDevelop 3 project files.
我们将重用上一个方案中的hello-world.cpp和CMakeLists.txt。唯一的区别是调用CMake,因为我们现在必须使用-G命令行界面开关显式地传递生成器。
1、首先,我们用以下命令来配置项目:
mkdir -p build
cd build
cmake -G Ninja ..
2、第二步,我们构建项目:
cmake --build .
我们已经看到配置步骤的输出与之前的方案相比没有变化。但是,编译步骤的输出和构建目录的内容将不同,因为每个生成器都有自己的特定文件集:
build.ninja和rules.ninja:包含Ninja的所有生成语句和生成规则。
CMakeCache.txt:CMake始终在此文件中生成自己的缓存,而不考虑所选的生成器。
CMakeFiles:包含配置期间由CMake生成的临时文件。
cmake-install.cmake:CMake脚本处理安装规则,在安装时使用。
注意:cmake --build . 将Ninja命令包装在一个统一的跨平台接口中。

构建并链接静态库和共享库
我们回到最初的示例。现在我们将引入一个类来包装要打印到屏幕上的消息,而不是为可执行文件提供一个单一的源文件。这是我们更新的hello-world.cpp:

#include "Message.hpp"
#include
#include
int main() {
    Message say_hello("Hello, CMake World!");
    std::cout << say_hello << std::endl;
    Message say_goodbye("Goodbye, CMake World");
    std::cout << say_goodbye << std::endl;
    return EXIT_SUCCESS;
}

我们回到最初的示例。现在我们将引入一个类来包装要打印到屏幕上的消息,而不是为可执行文件提供一个单一的源文件。这是我们更新的hello-world.cpp:
#include "Message.hpp"
#include
#include
int main() {
    Message say_hello("Hello, CMake World!");
    std::cout << say_hello << std::endl;
    Message say_goodbye("Goodbye, CMake World");
    std::cout << say_goodbye << std::endl;
    return EXIT_SUCCESS;
}
Message类封装了一个string,为<<运算符提供重载,并由两个源文件组成:Message.hpp头文件和相应的Message.cpp源文件。Message.hpp接口文件包含以下内容:
#pragma once
#include
#include
class Message{
public:
        Message(const std::string &m) : message_(m) {}
        friend std::ostream &operator<<(std::ostream &os, Message &obj) {
            return obj.printObject(os);
        }
private:
        std::string message_;
        std::ostream &printObject(std::ostream &os);
};
相应的实现包含在Message.cpp中:
#include "Message.hpp"
#include
#include
std::ostream &Message::printObject(std::ostream &os) {
    os << "This is my very nice message: " << std::endl;
    os << message_;
    return os;
}

这两个新文件也要被编译且我们必须相应地修改CMakeLists.txt。在本例中,我们希望首先将它们编译到库中,而不是直接编译到可执行文件中:
1、创建一个新的目标,此时是一个静态库。库的名称将是目标的名称,源代码如下所示:
add_library(message
    STATIC
    Message.hpp
    Message.cpp
)
2、创建hello-world可执行文件的目标是未修改的:
add_executable(hello-world hello-world.cpp)
3、最后,告诉CMake这个库目标将被连接到可执行程序目标:
target_link_libraries(hello-world message)
4、我们可以使用之前的命令配置并构建该项目。此时除了构建可执行程序以外,还有一个库被编译:
mkdir -p build
cd build
cmake ..
cmake --build .
前面的示例引入了两个新的命令:
add_library(message STATIC Message.hpp Message.cpp):这将生成将指定源编译到库中所需的生成工具指令。add_library的第一个参数是目标名称,在CMakeLists.txt中可以使用相同的名称来引用库。生成的库的实际名称将由CMake通过在前面添加前缀lib和适当的扩展名作为后缀来形成。库扩展是基于第二个参数(STATIC或SHARED)和操作系统来确定的。
target_link_libraries(hello-world message):将库链接到可执行程序中。此命令还将确保hello-world可执行文件正确地依赖于Message库。因此,在尝试将Message库链接到hello-world可执行文件之前,我们确保始终构建Message库。
编译成功后,构建目录将包含libmessage.a静态库(在GNU/Linux上)和hello-world可执行程序。
cmake接受对add_library的第二个参数有效的其他值,我们将在本书的其余部分中遇到这些值:
STATIC:将用于创建静态库,即对象文件的存档,用于链接其他目标,如可执行程序。
SHARED:将用于创建共享库,即可以动态链接并在运行时加载的库。从静态库切换到动态共享对象(DSO)与在CMakeLists.txt中使用add_library(message SHARED Message.hpp Message.cpp)一样简单。
OBJECT:可用于编译给定列表中的源文件,以便将库添加到对象文件中,但既不能将它们存档到静态库中,也不能将它们链接到共享对象中。如果需要一次性创建静态库和共享库,则使用对象尤其有用。我们将在这个方案中演示这一点。
MODULE:模块库也是DSO。与共享库不同,它们没有链接到项目中的任何其他目标,但可以稍后动态加载。这是构建运行时插件时要使用的参数。
CMake还能生成特殊类型的库。这些在构建系统中不产生输出,但在组织目标之间的依赖关系和构建需求方面非常有用:
IMPORTED:此类型的库目标表示位于项目外部的库。这种类型的库的主要用途是为上游包提供的项目的现有依赖项建模。因此,IMPORTED库将被视为不可变的。我们将在本书的其余部分展示使用IMPORTED库的示例。另请参见:https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#imported-targets
INTERFACE:这种特殊类型的CMake库类似于IMPORTED库,但它是可变的,且没有位置信息。它的主要用例是为项目之外的目标建模使用需求。我们将在Recipe 5中展示INTERFACE库的用例,在第11章”打包项目“中,将依赖项作为Conda包分发给一个项目。另请参见:https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#interface-libraries
ALIAS:顾名思义,这种类型的库为项目中预先存在的库目标定义了别名。因此,无法为IMPORTED库选择别名。另请参见:https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#alias-libraries
在这个例子中,我们已经使用add_library直接收集了源文件。在后面的章节中,我们将演示如何使用CMake命令target_sources来收集源文件,特别是在第7章”结构化项目“中。另请参见:https://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/,这进一步推动了使用target_sources命令。
现在让我们展示一下在CMake中可用的OBJECT库功能的使用。我们将使用相同的源文件,但修改CMakeLists.txt:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-03 LANGUAGES CXX)
add_library(message-objs
    OBJECT
    Message.hpp
    Message.cpp
    )
# this is only needed for older compilers
# but doesn't hurt either to have it
set_target_properties(message-objs
    PROPERTIES
    POSITION_INDEPENDENT_CODE 1
    )
add_library(message-shared
    SHARED
    $
    )
add_library(message-static
    STATIC
    $
    )
add_executable(hello-world hello-world.cpp)
target_link_libraries(hello-world message-static)
首先,请注意add_library命令已更改为add_library(message-objs OBJECT Message.hpp Message.cpp)。此外,我们必须确保编译到对象文件时生成与位置无关的代码。这是通过使用set_target_properties命令设置message-objs目标的相应属性来完成的。
注意:只有在某些平台和/或使用较旧的编译器时,才可能出现为目标显式设置位置独立代码属性的需要。
这个对象库现在可以用来获取静态库(称为message-static)和共享库(称为message-shared)。必须注意用于引用对象库的生成器表达式语法:
$。生成器表达式是CMake在生成时计算的构造,在配置时之后立即生成特定于配置的生成输出。另请参见:https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html。我们将在第5章后面深入研究生成器表达式,配置时和构建时操作。最后,hello-world可执行文件与message库的静态版本链接。
是否可以让CMake生成同名的两个库?换句话说,它们都可以被称为message而不是message-static和message-shared吗?我们需要修改这两个目标的属性:
add_library(message-shared
    SHARED
    $
    )
set_target_properties(message-shared
    PROPERTIES
    OUTPUT_NAME "message"
    )
add_library(message-static
    STATIC
    $
    )
set_target_properties(message-static
    PROPERTIES
    OUTPUT_NAME "message"
    )
我们能联系到DSO吗?它取决于操作系统和编译器:
1、在GNU/Linux和MacOS上,无论选择哪种编译器,它都能工作。
2、在Windows上,它不能与Visual Studio一起工作,但可以与MinGW和MSYS2一起工作。
为什么?生成好的DSO需要程序员限制符号可见性。这是在编译器的帮助下实现的,但是在不同的操作系统和编译器上,约定是不同的。CMake有一个强大的机制来处理这个问题,我们将在第10章”编写一个安装程序“中解释它是如何工作的。

条件控制编译
到目前为止,我们已经研究了相当简单的项目,其中CMake的执行流程是线性的:从一组源文件到单个可执行文件,可能通过静态库或共享库。为了确保完全控制构建项目、配置、编译和链接所涉及的所有步骤的执行流程,CMake提供了自己的语言。在这个方案中,我们将探讨条件构造if-elseif-else-endif的用法。
让我们从与前一个方案相同的源代码开始。我们希望能够在两种行为之间切换:
1、将Message.hpp和Message.cpp构建到静态或共享库中,然后将生成的库链接到hello-world可执行文件中。
2、将Message.hpp、Message.cpp和hello-world.cpp构建到单个可执行文件中,而不生成库。
让我们构造CMakeLists.txt来实现这一点:
1、我们首先定义最低的CMake版本、项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-04 LANGUAGES CXX)
2、我们引入一个新的变量,USE_LIBRARY。这是一个逻辑变量,其值将设置为OFF。我们还为用户打印其值:
set(USE_LIBRARY OFF)
message(STATUS "Compile sources into a library? ${USE_LIBRARY}")
3、将在CMake中定义的BUILD_SHARED_LIBS全局变量设置为OFF。调用add_library并省略第二个参数将构建静态库:
set(BUILD_SHARED_LIBS OFF)
4、然后,我们引入一个变量,_sources”,列出Message.hpp和Message.cpp:
list(APPEND _sources Message.hpp Message.cpp)
5、然后,我们根据USE_LIBRARY的值引入if-else语句。如果逻辑切换为真,Message.hpp和Message.cpp将打包到库中:
if(USE_LIBRARY)
    # add_library will create a static library
    # since BUILD_SHARED_LIBS is OFF
    add_library(message ${_sources})
    add_executable(hello-world hello-world.cpp)
    target_link_libraries(hello-world message)
else()
    add_executable(hello-world hello-world.cpp ${_sources})
endif()
6、我们可以使用相同的命令集再次构建。由于USE_LIBRARY被设置为OFF,将从所有源编译hello-world可执行文件。这可以通过在GNU/Linux上运行objdump -x命令来验证。
我们引入了两个变量:USE_LIBRARY和BUILD_SHARED_LIBS。他们两个都被设置为OFF。如CMake语言文档中所详细说明的,真或假值可以用多种方式表示:
如果将逻辑变量设置为以下任一值,则逻辑变量为真:1、ON、YES、TRUE、Y或非零数字。
如果将逻辑变量设置为以下任何一项,则逻辑变量为假:0、OFF、NO、FALSE、N、IGNORE、NOTFOUND、空字符串,或以后缀-NOTFOUND结尾。
USE_LIBRARY变量将在第一个和第二个行为之间切换。BUILD_SHARED_LIBS是CMake提供的一个全局标志。记住,可以在不传递STATIC/SHARED/OBJECT参数的情况下调用add_library命令。这是因为在内部查找BUILD_SHARED_LIBS全局变量;如果为false或undefined,将生成静态库。
这个例子表明,可以引入条件来控制CMake中的执行流。但是,当前设置不允许从外部设置切换,也就是说,不需要手动修改CMakeLists.txt。原则上,我们希望能够向用户公开所有切换,这样就可以在不修改构建系统代码的情况下调整配置。我们马上演示如何做到这一点。

向用户演示选项
让我们看一下前面方案中的静态/共享库示例。现在,我们不再将USE_LIBRARY硬编码为ON或OFF,而是将其作为option公开,默认值可以从外部更改:
1、用option替换上一个方案的set(USE_LIBRARY OFF)命令。该选项将具有相同的名称,其默认值将为“OFF”:
option(USE_LIBRARY "Compile sources into a library" OFF)
2、现在,我们可以通过-D命令行界面选项将信息传递给CMake来切换库的生成:
mkdir -p build
cd build
cmake -D USE_LIBRARY=ON ..
cmake --build .
-D开关用于为CMake设置任何类型的变量:逻辑、路径等。
option( "help string" [initial value])
是表示选项的变量的名称。
“help string”是记录选项的字符串。此文档在基于终端或CMake的图形用户界面中可见。
[initial value]是选项的默认值,无论是ON还是OFF。
有时需要引入取决于其他选项的值的选项。在我们的示例中,我们可能希望提供生成静态库或共享库的选项。但是,如果未将USE_LIBRARY逻辑设置为ON,则此选项将没有意义。CMake提供了cmake_dependent_option()命令来定义依赖于其他选项的选项:
include(CMakeDependentOption)
# second option depends on the value of the first
cmake_dependent_option(
    MAKE_STATIC_LIBRARY "Compile sources into a static library" OFF
    "USE_LIBRARY" ON
)
# third option depends on the value of the first
cmake_dependent_option(
    MAKE_SHARED_LIBRARY "Compile sources into a shared library" ON
    "USE_LIBRARY" ON
)
如果USE_LIBRARY为ON,则MAKE_STATIC_LIBRARY默认为OFF,而MAKE_SHARED_LIBRARY默认为ON。所以我们可以运行这个:
cmake -D USE_LIBRARY=OFF -D MAKE_SHARED_LIBRARY=ON ..
因为USE_LIBRARY仍然设置为OFF,所以仍然不会构建库。
如前所述,CMake具有适当的机制,可以通过包含模块来扩展其语法和功能,这些模块要么随CMake本身一起提供,要么随自定义模块一起提供。在本例中,我们包含一个名为CMakeDependentOption的模块。如果没有include语句,cmake_dependent_option()命令将无法使用。另请参见:https://cmake.org/cmake/help/latest/module/cmakefendentoption.html

指定编译器
到目前为止我们还没有考虑到的一个方面是编译器的选择。CMake非常复杂,可以根据平台和生成器选择最合适的编译器。CMake还可以将编译器标志设置为一组正常的默认值。然而,我们通常希望控制编译器的选择,在这个方案中,我们将展示如何完成这一点。在后面的方案中,我们还将考虑构建类型的选择,并演示如何控制编译器标志。
我们如何选择特定的编译器?例如,如果我们想使用Intel或Portland Group编译器呢?CMake将每种语言的编译器存储在CMAKE__COMPILER变量中,其中是任何受支持的语言,用于CXX、C或Fortran。用户可以通过以下两种方式之一设置此变量:
1、在命令行界面中使用-D选项,例如:
cmake -D CMAKE_CXX_COMPILER=clang++ ..
通过为C++编译器导出环境变量CXX、C编译器的CC以及Fortran编译器的FC。例如,使用这个命令以使用Clang++作为C++编译器:
env CXX=clang++ cmake ..
到目前为止讨论的任何方案都可以通过传递适当的选项配置为与任何其他编译器一起使用。
在这里,我们假设在CMake进行查找的标准路径中可以使用其他编译器。如果不是这样,用户需要将完整路径传递给编译器可执行文件或包装器。
在哪里可以找到CMake将为我们的平台获取哪些默认编译器和编译器标志?CMake提供--system-information标志,该标志将系统的所有信息转储到屏幕或文件中。要查看此信息,请尝试以下操作:
cmake --system-information information.txt
在文件中搜索(在本例中是information.txt),您将找到CMAKE_CXX_COMPILER、CMAKE_C_COMPILER和CMAKE_Fortran_COMPILER选项的默认值及其默认标志。我们看看下一个方案中的标记。
CMake提供其他变量与编译器交互:
CMAKE__COMPILER_LOADED:如果为项目启用了,则此设置为TRUE。
CMAKE__COMPILER_ID:编译器标识字符串对于编译器供应商是独有的。例如,GCC之于GNU Compiler Collection,AppleClang之于MacOS上的Clang,还有MSVC之于Microsoft Visual Studio。但是,请注意,不能保证为所有编译器或语言定义此变量。
CMAKE_COMPILER_IS_GNU:如果语言的编译器是GNU Compiler Collection的一部分,则此逻辑变量设置为TRUE。注意,变量名称的部分遵循GNU约定:它是C语言的CC,C++语言的CXX,或者Fortran语言的G77。
CMAKE__COMPILER_VERSION:此变量保存一个字符串,其中包含给定语言的编译器版本。版本信息以major[.minor[.patch[.tweak]]]的格式给出。但是,对于CMAKE__COMPILER_ID,不保证为所有编译器或语言定义此变量。
我们可以尝试用不同的编译器配置以下示例CMakeLists.txt。在本例中,我们将使用CMake变量来探测我们使用的编译器和版本:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-06 LANGUAGES C CXX)
message(STATUS "Is the C++ compiler loaded? ${CMAKE_CXX_COMPILER_LOADED}")
if(CMAKE_CXX_COMPILER_LOADED)
    message(STATUS "The C++ compiler ID is: ${CMAKE_CXX_COMPILER_ID}")
    message(STATUS "Is the C++ from GNU? ${CMAKE_COMPILER_IS_GNUCXX}")
    message(STATUS "The C++ compiler version is: ${CMAKE_CXX_COMPILER_VERSION}")
endif()
message(STATUS "Is the C compiler loaded? ${CMAKE_C_COMPILER_LOADED}")
if(CMAKE_C_COMPILER_LOADED)
    message(STATUS "The C compiler ID is: ${CMAKE_C_COMPILER_ID}")
    message(STATUS "Is the C from GNU? ${CMAKE_COMPILER_IS_GNUCC}")
    message(STATUS "The C compiler version is: ${CMAKE_C_COMPILER_VERSION}")
endif()
请注意,此示例不包含任何目标,因此没有要构建的内容,我们只关注配置步骤:
mkdir -p build
cd build
cmake ..
当然,输出将取决于可用和选择的编译器和编译器版本。

切换生成类型
CMake 具有生成类型或配置的概念,如调试、发布等。在一个配置中,可以收集调试或发布版本的相关选项或属性,如编译器和链接器标志。管理生成系统时要使用的配置的变量是CMAKE_BUILD_TYPE。默认情况下,此变量为空,CMake识别的值为:
1、Debug:无需优化且使用调试符号即可构建库或可执行文件,
2、Release:用于构建库或可执行文件,具有优化功能,无需调试符号,
3、RelWithDebInfo:用于构建您的库或可执行文件,具有较不激进的优化和调试符号,
4、MinSizeRel:使用不增加对象代码大小的优化来构建库或可执行文件。
在此方案中,我们将演示如何为示例项目设置生成类型:
1、我们首先定义最小CMake版本、项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-07 LANGUAGES C CXX)
2、然后,我们设置默认生成类型(在本例中为Release),并将其打印在用户的消息中。请注意,变量设置为CACHE变量,以便随后可以通过缓存对其进行编辑:
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
3、最后,我们将CMake设置的相应编译标志打印为生成类型的函数:
message(STATUS "C flags, Release configuration: ${CMAKE_C_FLAGS_RELEASE}")
message(STATUS "C flags, Release configuration with Debug info: ${CMAKE_C_FLAGS_RELWITHDEBINFO}")
message(STATUS "C flags, minimal Release configuration: ${CMAKE_C_FLAGS_MINSIZEREL}")
message(STATUS "C++ flags, Debug configuration: ${CMAKE_CXX_FLAGS_DEBUG}")
message(STATUS "C++ flags, Release configuration: ${CMAKE_CXX_FLAGS_RELEASE}")
message(STATUS "C++ flags, Release configuration with Debug info: ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
message(STATUS "C++ flags, minimal Release configuration: ${CMAKE_CXX_FLAGS_MINSIZEREL}")
4、现在,让我们验证默认配置的输出:
mkdir -p build
cd build
cmake ..
5、现在,让我们切换生成类型:
cmake -D CMAKE_BUILD_TYPE=Debug ..
我们已经展示了变量CMAKE_BUILD_TYPE(链接:https://cmake.org/cmake/help/v3.5/variable/CMAKE_BUILD_TYPE.html)如何定义生成的构建系统的配置。在Release和Debug配置中构建项目通常很有帮助,例如在评估编译器优化级别的影响时。对于单配置生成器,如Unix Makefiles、MSYS Makefiles或Ninja,这需要运行CMake两次,这是项目的完整重新配置。但是,CMake还支持多个配置生成器。这些通常是集成开发环境提供的项目文件,最显著的是Visual Studio和Xcode,它们可以同时处理多个配置。这些生成器的可用配置类型可以使用CMAKE_CONFIGURATION_TYPES变量进行调整,该变量将接受值列表(链接:https://cmake.org/cmake/help/v3.5/variable/CMAKE_CONFIGURATION_TYPES.html)。

控制编译器标志
前面的方案演示如何探测CMake以获取有关编译器的信息,以及如何为项目中的所有目标优化编译器优化。后一个任务是控制项目中使用哪些编译器标志的一般需求的子集。CMake为调整或扩展编译器标志提供了很大的灵活性,您可以在两种主要方法之间进行选择:
CMake将编译选项视为目标的属性。因此,可以基于每个目标设置编译选项,而无需重写CMake默认值。
您可以使用-D命令行界面开关直接修改CMAKE__FLAGS_变量。这些将影响项目中的所有目标,并覆盖或扩展CMake默认值。
在此方案中,我们将展示这两种方法。
我们将编译一个示例程序来计算不同几何形状的面积。该代码在名为compute-areas.cpp的文件中有一个main函数:
#include "geometry_polygon.hpp"
#include "geometry_rhombus.hpp"
#include "geometry_square.hpp"
#include
#include
int main() {
    using namespace geometry;
    double radius = 2.5293;
    double A_circle = area::circle(radius);
    std::cout << "A circle of radius " << radius << " has an area of " << A_circle
      << std::endl;
    int nSides = 19;
    double side = 1.29312;
    double A_polygon = area::polygon(nSides, side);
    std::cout << "A regular polygon of " << nSides << " sides of length " << side
      << " has an area of " << A_polygon << std::endl;
    double d1 = 5.0;
    double d2 = 7.8912;
    double A_rhombus = area::rhombus(d1, d2);
    std::cout << "A rhombus of major diagonal " << d1 << " and minor diagonal " << d2
      << " has an area of " << A_rhombus << std::endl;
    double l = 10.0;
    double A_square = area::square(l);
    std::cout << "A square of side " << l << " has an area of " << A_square
      << std::endl;
    return EXIT_SUCCESS;
}
各种函数的实现包含在其他文件中:每个几何形状都有一个头文件和一个相应的源文件。我们总共有四个头文件和五个源文件要编译:
.
├── CMakeLists.txt
├── compute-areas.cpp
├── geometry_circle.cpp
├── geometry_circle.hpp
├── geometry_polygon.cpp
├── geometry_polygon.hpp
├── geometry_rhombus.cpp
├── geometry_rhombus.hpp
├── geometry_square.cpp
└── geometry_square.hpp
我们不会为所有这些文件提供列表,而是给读者提供:https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-08。
现在,我们已经具备了源代码,我们的目标是配置项目并试验编译器标志:
1、我们设置需要的CMake的最小版本:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
2、我们定义项目和语言的名称:
project(recipe-08 LANGUAGES CXX)
3、然后,我们打印当前编译器标志集。CMake将对所有C++目标使用这些:
message("C++ compiler flags: ${CMAKE_CXX_FLAGS}")
4、我们为目标准备一个标志列表。其中一些在Windows上不可用,我们确保说明这种情况:
list(APPEND flags "-fPIC" "-Wall")
if(NOT WIN32)
    list(APPEND flags "-Wextra" "-Wpedantic")
endif()
5、我们添加新的目标,geometry库,并列出其源文件依赖项:
add_library(geometry
    STATIC
    geometry_circle.cpp
    geometry_circle.hpp
    geometry_polygon.cpp
    geometry_polygon.hpp
    geometry_rhombus.cpp
    geometry_rhombus.hpp
    geometry_square.cpp
    geometry_square.hpp
    )
6、我们为这个库目标设置编译选项:
target_compile_options(geometry
    PRIVATE
    ${flags}
    )
7、我们也为compute-areas可执行文件添加目标:
add_executable(compute-areas compute-areas.cpp)
8、我们也为可执行文件目标设置编译选项:
target_compile_options(compute-areas
    PRIVATE
    "-fPIC"
    )
9、最后,我们将可执行文件链接到geometry库:
target_link_libraries(compute-areas geometry)
在此示例中,警告标志-Wall、-Wextra和-Wpedantic将添加到geometry目标的编译选项中;compute-areas和geometry目标都将使用-fPIC标志。编译选项可以添加三个级别的可见性:INTERFACE、PUBLIC和PRIVATE。
可见性级别具有以下含义:
使用PRIVATE属性,编译选项将仅应用于给定目标,而不是应用于使用它的其他目标。在我们的示例中,在geometry目标上设置的编译器选项不会由compute-areas继承,即使compute-areas将链接到geometry库。
使用INTERFACE属性,将仅将给定目标上的编译选项应用于使用它的目标。
使用PUBLIC属性,编译选项将应用于给定的目标和所有其他使用该目标的目标。
目标属性的可见性级别是CMake现代用法的核心,我们将在本书中经常和广泛地重新讨论此主题。以这种方式添加编译选项不会污染CMAKE__FLAGS_全局CMake变量,并使您能够对哪些目标使用哪些选项进行精细控制。
我们如何验证标志是否按预期正确使用?或者换句话说,您如何发现CMake项目实际使用哪些编译标志?一种方法如下,它使用CMake将其他参数(本例中为环境变量 VERBOSE=1)传递给本机构建工具:
mkdir -p build
cd build
cmake ..
cmake --build . -- VERBOSE=1
前面的输出确认编译标志已根据我们的指示正确设置。
控制编译器标志的第二种方法不涉及对CMakelists.txt的修改。如果想要修改此项目中geometry和compute-areas目标的编译器选项,那么使用附加参数调用CMake非常简单:
cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..
正如您可能已经猜到的,这个命令将编译项目,停用异常和运行时类型标识(RTTI)。
这两种方法也可以耦合。您可以全局使用一组基本标志,同时保持对每个目标的控制。我们可以使用CMakeLists.txt并运行以下命令:
cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..
这将用-fno-exceptions -fno-rtti -fPIC -Wall - Wextra - Wpedantic配置geometry目标,同时用-fno-exceptions -fno-rtti -fPIC配置compute-areas。
大多数时候,标志是特定于编译器的。我们目前的示例仅适用于GCC和Clang;来自其他供应商的编译器不会理解这些标志中的许多(如果不是全部)。显然,如果一个项目旨在真正跨平台,这个问题必须得到解决。有三种方法可以解决。
最典型的方法是将所需编译器标志的列表追加到每个配置类型CMake变量,即CMAKE__FLAGS_。这些标志设置为已知适用于给定编译器供应商的内容,因此将包含在检查CMAKE__COMPILER_ID变量的if-endif子句中,例如:
if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
    list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions")
    list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wsuggest-final-types" "-Wsuggest-final-methods" "-Wsuggest-override")
    list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
    list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions" "-Qunused-arguments" "-fcolor-diagnostics")
    list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wdocumentation")
    list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()
更精细的方法不会篡改CMAKE__FLAGS_变量,而是定义特定于项目的标志列表:
set(COMPILER_FLAGS)
set(COMPILER_FLAGS_DEBUG)
set(COMPILER_FLAGS_RELEASE)
if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
    list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions")
    list(APPEND CXX_FLAGS_DEBUG "-Wsuggest-final-types" "-Wsuggest-final-methods" "-Wsuggest-override")
    list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
    list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions" "-Qunused-arguments" "-fcolor-diagnostics")
    list(APPEND CXX_FLAGS_DEBUG "-Wdocumentation")
    list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()
稍后,它使用生成器表达式根据每个配置和每个目标设置编译器标志:
target_compile_option(compute-areas
    PRIVATE
    ${CXX_FLAGS}
    "$<$:${CXX_FLAGS_DEBUG}>"
    "$<$:${CXX_FLAGS_RELEASE}>"
    )
我们在当前方案中显示了这两种方法,并明确推荐了后者(特定于项目的变量和target_compile_options)而不是前者(CMake变量)。
这两种方法都起作用,并广泛用于许多项目。然而,它们也有缺点。正如我们已经提到的,CMAKE__COMPILER_ID不能保证为所有编译器供应商定义。此外,某些标志可能会被弃用,或者可能在编译器的更高版本中引入。与CMAKE__COMPILER_ID类似,不保证为所有语言和供应商定义CMAKE__COMPILER_VERSION变量。尽管检查这些变量相当流行,但我们认为,一个更健壮的替代方法是检查所需的标志集是否与给定编译器一起使用,以便项目中仅实际使用有效的工作标志。结合使用特定于项目的变量、target_compile_options和生成器表达式,此方法非常强大。我们将在Recipe 3中演示如何使用此检查和设置模式,在第7章”构建项目“中编写函数以测试和设置编译器标志。

为语言设置标准
编程语言有不同的标准,提供新语言构造和改进语言构造的不同版本。通过设置适当的编译器标志来实现启用新标准。我们在前面的方案中已经表明了如何按目标或全局实现。CMake的3.1版本引入了一种独立于平台和编译器的机制,用于设置C++和C语言标准:为目标设置_STANDARD属性。
对于以下示例,我们将要求符合C++14标准或更高版本的C++编译器。此方案的代码定义了动物的多态层次结构。我们使用std::unique_ptr来表示层次结构中的基类:
std::unique_ptr cat = Cat("Simon");
std::unique_ptr dog = Dog("Marlowe);
我们使用工厂方法的实现,而不是为各种子类型显式使用构造函数。工厂使用 C++11可变模板实现。它保存继承层次结构中每个对象的创建函数映射:typedef std::function(const std::string &)> CreateAnimal;
它基于预先分配的标记分派它们,以便创建对象如下:
std::unique_ptr simon = farm.create("CAT", "Simon");
std::unique_ptr marlowe = farm.create("DOG", "Marlowe");
标记和创建功能在使用前已注册到工厂:
Factory farm;
farm.subscribe("CAT", [](const std::string & n) { return std::make_unique(n); });
farm.subscribe("DOG", [](const std::string & n) { return std::make_unique(n); });
我们使用C++11 lambda函数定义创建函数。请注意使用 std:make_unique,以避免引入裸new运算符。此帮助程序在C++14中引入。
我们将逐步构造CMakeLists.txt,并演示如何要求特定的标准(在本例中为C++14):
1、我们声明最低要求的CMake版本、项目名称和语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-09 LANGUAGES CXX)
2、我们请求在Windows上导出所有库符号:
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
3、我们需要为库添加一个目标。这将把源文件编译为共享库:
add_library(animals
    SHARED
    Animal.cpp
    Animal.hpp
    Cat.cpp
    Cat.hpp
    Dog.cpp
    Dog.hpp
    Factory.hpp
    )
4、现在,我们为目标设置CXX_STANDARD、CXX_EXTENSIONS和CXX_STANDARD_REQUIRED属性。我们还设置了”POSITION_INDEPENDENT_CODE“属性,以避免在使用某些编译器构建DSO时出现问题:
set_target_properties(animals
    PROPERTIES
    CXX_STANDARD 14
    CXX_EXTENSIONS OFF
    CXX_STANDARD_REQUIRED ON
    POSITION_INDEPENDENT_CODE 1
    )
5、然后,我们为animal-farm可执行文件添加新目标并设置其属性:
add_executable(animal-farm animal-farm.cpp)
set_target_properties(animal-farm
    PROPERTIES
    CXX_STANDARD 14
    CXX_EXTENSIONS OFF
    CXX_STANDARD_REQUIRED ON
    )
6、最后,我们将可执行文件链接到库:
target_link_libraries(animal-farm animals)
7、让我们检查我们的例子,看看猫和狗怎么说:
cd build
cmake ..
cmake --build .
./animal-farm
在步骤4和5中,我们为animals和animal-farm目标设定了许多属性:
CXX_STANDARD:规定了我们想要的标准。
CXX_EXTENSIONS:告诉CMake仅使用启用ISO C++标准的编译器标志,而不使用特定于编译器的扩展。
CXX_STANDARD_REQUIRED:指定所选标准的版本是必需的。如果此版本不可用,CMake将停止配置,并出现错误。当此属性设置为OFF时,CMake将查找标准的下一个最新版本,直到设置正确的标志。这意味着首先查找C++14,然后查找C++11,然后查找C++98。
CMake通过引入编译功能的概念,对语言标准提供了更精细的控制级别。这些是语言标准引入的功能,如C++11中的可变模板和lambas,以及C++14中的自动返回类型推减。您可以使用target_compile_features()命令要求特定目标提供某些功能,CMake将自动为标准设置正确的编译器标志。也可以让CMake为可选的编译器功能生成兼容性标头。

使用控制流构造
我们在本章以前的配方中使用了if-elseif-endif构造。CMake还提供用于创建循环的语言工具:foreach-endforeach和while-endwhile。两者都可以与break相结合,以便尽早从封闭循环中断开。此方案将向您展示如何使用foreach循环访问源文件的列表。我们将应用这样的循环来降低一组源文件的编译器优化,而不引入新的目标。
我们将重用Recipe 8中引入的geometry示例,控制编译器标志。我们的目标是通过将某些源文件收集到列表中来微调某些源文件的编译器优化。
以下是CMakeLists.txt中应遵循的详细步骤:
1、与Recipe 8中控制编译器标志一样,我们指定CMake、项目名称和语言的最低必需版本,并声明geometry库目标:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-10 LANGUAGES CXX)
add_library(geometry
    STATIC
    geometry_circle.cpp
    geometry_circle.hpp
    geometry_polygon.cpp
    geometry_polygon.hpp
    geometry_rhombus.cpp
    geometry_rhombus.hpp
    geometry_square.cpp
    geometry_square.hpp
    )
2、我们决定使用-O3编译器优化级别编译库。这被设置为目标上的PRIVATE编译器选项:
target_compile_options(geometry
    PRIVATE
    -O3
    )
3、然后,我们生成要以较低优化编译的源文件列表:
list(
    APPEND sources_with_lower_optimization
    geometry_circle.cpp
    geometry_rhombus.cpp
)
4、我们循环这些源文件,以调整其优化级别到-O2。这是使用其源文件属性完成的:
message(STATUS "Setting source properties using IN LISTS syntax:")
foreach(_source IN LISTS sources_with_lower_optimization)
    set_source_files_properties(${_source} PROPERTIES COMPILE_FLAGS -O2)
    message(STATUS "Appending -O2 flag for ${_source}")
endforeach()
5、为了确保设置了源属性,我们再次循环,并在每个源文件上打印COMPILE_FLAGS属性:
message(STATUS "Querying sources properties using plain syntax:")
foreach(_source ${sources_with_lower_optimization})
    get_source_file_property(_flags ${_source} COMPILE_FLAGS)
    message(STATUS "Source ${_source} has the following extra COMPILE_FLAGS: ${_flags}")
endforeach()
6、最后,我们添加compute-areas可执行目标,并将其链接到geometry库:
add_executable(compute-areas compute-areas.cpp)
target_link_libraries(compute-areas geometry)
7、让我们验证标志在配置步骤中设置正确:
mkdir -p build
cd build
cmake ..
8、最后,还使用VERBOSE=1检查生成步骤。您将看到-O2标志追加到-O3标志,但最后一个优化级别标志(在本例中为-O2)”赢了“:
cmake --build . -- VERBOSE=1
foreach-endeach语法可用于表示某些任务的重复数超过变量列表。在我们的例子中,我们使用它来操作、设置和获取项目中特定文件的编译器标志。此CMake代码段引入了另外两个新命令:
set_source_files_properties(file PROPERTIES property value):将属性设置为给定文件的传递值。与目标类似,文件在CMake中也有属性。这允许对构建系统生成进行极其精细的控制。源文件的可用属性列表可在此处找到:https://cmake.org/cmake/help/v3.5/manual/cmake-properties.7.html#source-file-properties。
get_source_file_property(VAR file property):检索给定文件的所需属性的值并将其存储在CMake VAR变量中。
foreach()构造可以有四种不同的使用方式:
foreach(loop_var arg1 arg2 ...):其中提供了循环变量和显式项列表。在打印sources_with_lower_optimization中的项的编译器标志集时使用此形式。请注意,如果项列表位于变量中,则必须显式展开;也就是说,${sources_with_lower_optimization}必须作为参数传递。
通过指定范围(如foreach(loop_var RANGE total)或foreach(loop_var RANGE start stop [step]))来作为整数数字的循环。
作为列表值变量的循环,例如foreach(loop_var IN LISTS [list1 [...] ])。参数被解释为列表,其内容会自动相应地展开。
作为对项的循环,例如foreach(loop_var IN ITEMS [item1 [...] ])。参数的内容不会展开。

检测环境
在本章中,我们将介绍以下方案:
发现操作系统
处理依赖于平台的源代码
处理依赖于编译器的源代码
发现主机处理器体系结构
发现主机处理器指令集
为Eigen库启用矢量化
尽管CMake是跨平台的,在我们的项目中,我们努力使源代码跨平台、操作系统和编译器移植;但有时源代码不能完全移植;例如,当使用依赖于供应商的扩展时,我们可能会发现有必要根据平台进行稍微不同的配置和/或构建代码。这与遗留代码或交叉编译时特别相关,我们将在第13章”替代生成器和交叉编译“中返回一个主题。了解处理器指令集以优化特定目标平台的性能也是有益的。本章介绍检测此类环境的方案,并就如何实施此类解决方案提供建议。
发现操作系统
CMake是一套跨平台工具。但是,了解配置或生成步骤在哪个操作系统 (OS) 上执行可能非常有用。此类操作系统发现可用于调整特定操作系统的CMake代码、根据操作系统启用条件编译,或者使用特定于编译器的扩展(如果可用或必要)。在此方案中,我们将演示如何使用CMake来检测操作系统,该示例不需要编译任何源代码。为简单起见,我们将只考虑配置步骤。
我们将使用非常简单的CMakeLists.txt演示操作系统发现:
1、我们首先定义最小CMake版本和项目名称。请注意,我们的语言要求为”NONE“:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-01 LANGUAGES NONE)
2、然后,我们希望打印自定义消息,具体取决于检测到的操作系统:
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    message(STATUS "Configuring on/for Linux")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    message(STATUS "Configuring on/for macOS")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    message(STATUS "Configuring on/for Windows")
elseif(CMAKE_SYSTEM_NAME STREQUAL "AIX")
    message(STATUS "Configuring on/for IBM AIX")
else()
    message(STATUS "Configuring on/for ${CMAKE_SYSTEM_NAME}")
endif()
在测试之前,请先检查前面的代码块,并考虑您希望在系统上出现什么行为。
3、现在,我们准备测试它并配置项目:
mkdir -p build
cd build
cmake ..
4、在CMake输出中,有一行很有趣:在Linux系统上,这是感兴趣的行(在其他系统上,输出希望会有所不同):
-- Configuring on/for Linux
CMake正确定义目标操作系统的CMAKE_SYSTEM_NAME,因此通常不需要使用自定义命令、工具或脚本来查询此信息。然后,此变量的值可用于实现特定于OS的条件和解决方法。在具有uname命令的系统上,此变量设置为uname -s的输出。该变量在MacOS上被设置为”Darwin“。在Linux和Windows上,它分别评估为”Linux“和”Windows“。现在,我们知道如何在特定操作系统上执行特定的CMake代码(如果需要)。当然,我们应该尽量减少这种自定义,以简化迁移到新平台的过程。

处理依赖于平台的源代码
理想情况下,我们应该避免依赖于平台的源代码,但有时我们别无选择,特别是当我们被赋予配置和编译我们自己没有编写的代码时。在这个方法中,我们将演示如何使用CMake根据操作系统有条件地编译源代码。
对于本例,我们将从第1章修改hello-world.cpp示例代码,从一个简单的可执行文件修改为库,方案1,将单个源文件编译为一个可执行文件:
#include
#include
#include
std::string say_hello() {
#ifdef IS_WINDOWS
    return std::string("Hello from Windows!");
#elif IS_LINUX
    return std::string("Hello from Linux!");
#elif IS_MACOS
    return std::string("Hello from macOS!");
#else
    return std::string("Hello from an unknown system!");
#endif
}
int main() {
    std::cout << say_hello() << std::endl;
    return EXIT_SUCCESS;
}
让我们构建一个对应的CMakeLists.txt实例,它将使我们能够根据目标操作系统有条件地编译源代码:
1、我们首先设置最小CMake版本、项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-02 LANGUAGES CXX)
2、然后定义可执行文件及其对应的源文件:
add_executable(hello-world hello-world.cpp)
3、然后,通过定义以下目标编译定义,让预处理器知道系统名称:
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    target_compile_definitions(hello-world PUBLIC "IS_LINUX")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    target_compile_definitions(hello-world PUBLIC "IS_MACOS")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    target_compile_definitions(hello-world PUBLIC "IS_WINDOWS")
endif()
在继续之前,首先检查前面的表达式,并考虑您在系统上期望的行为。
4、现在我们准备测试它并配置项目:
mkdir -p build
cd build
cmake ..
cmake --build .
./hello-world
在Windows系统上,您将看到”Hello from Windows!“;其他操作系统将产生不同的输出。
hello-world.cpp示例中有趣的部分是基于预处理器定义IS_WINDOWS、IS_LINUX或IS_MACOS的条件编译:
std::string say_hello() {
#ifdef IS_WINDOWS
    return std::string("Hello from Windows!");
#elif IS_LINUX
    return std::string("Hello from Linux!");
#elif IS_MACOS
    return std::string("Hello from macOS!");
#else
    return std::string("Hello from an unknown system!");
#endif
}
这些定义在配置时由CMakeLists.txt中的CMake定义,在传递给预处理器之前,使用target_compile_definitions定义。我们本可以实现一个更紧凑的表达式,而无需重复if-endif语句,我们将在下一个方案中演示此重构。我们还可以将if-endif语句加入到一个if-elseif-elseif-endif语句中。在此阶段,我们应该指出,我们可以使用add_definition(当然,根据相关平台调整定义)而不是使用target_compile_definitions来设置定义。使用add_definition的缺点是,它修改了整个项目的编译定义,而目标定义则使我们有可能将定义的范围限制为特定目标,并限制通过使用PRIVATE、PUBLIC或INTERFACE限定符来可见性这些定义。这些限定符对编译器标志的含义相同,正如我们在第1章”从简单可执行到库“、Recipe 8、控制编译器标志中所看到的那样:
使用PRIVATE限定符时,编译定义将仅应用于给定目标,而不会由使用它的其他目标应用。
使用INTERFACE限定符时,将仅将给定目标上的定义编译应用于使用它的目标。
使用PUBLIC限定符时,编译定义将应用于给定目标以及使用它的所有其他目标。

处理依赖于编译器的源代码
此方案与前一个方案类似,即我们将使用CMake来容纳依赖于环境的条件源代码的编译:在这种情况下,它将依赖于所选的编译器。同样,为了便于移植,这是我们在编写新代码时尽量避免的情况,但这也是我们几乎保证迟早会满足的情况,尤其是在使用旧代码或处理依赖于编译器的工具时,如sanitizers。从本章和前一章的方案,我们拥有实现这一目标的所有原料。不过,讨论处理依赖于编译器的源代码的问题将非常有用,因为我们将有机会介绍CMake的一些新方面。
在这个方案中,我们将从C++中的一个例子开始,稍后我们将演示一个Fortran示例,并尝试重构和简化CMake代码。
让我们考虑以下hello-world.cpp源代码:
#include
#include
#include
std::string say_hello() {
#ifdef IS_INTEL_CXX_COMPILER
    // only compiled when Intel compiler is selected
// such compiler will not compile the other branches
return std::string("Hello Intel compiler!");
#elif IS_GNU_CXX_COMPILER
    // only compiled when GNU compiler is selected
// such compiler will not compile the other branches
return std::string("Hello GNU compiler!");
#elif IS_PGI_CXX_COMPILER
    // etc.
return std::string("Hello PGI compiler!");
#elif IS_XL_CXX_COMPILER
    return std::string("Hello XL compiler!");
#else
    return std::string("Hello unknown compiler - have we met before?");
#endif
}
int main() {
    std::cout << say_hello() << std::endl;
    std::cout << "compiler name is " COMPILER_NAME << std::endl;
    return EXIT_SUCCESS;
}
我们还将使用相应的Fortran示例(hello-world.F90):
program hello
    implicit none
#ifdef IS_Intel_FORTRAN_COMPILER
    print *, 'Hello Intel compiler!'
#elif IS_GNU_FORTRAN_COMPILER
    print *, 'Hello GNU compiler!'
#elif IS_PGI_FORTRAN_COMPILER
    print *, 'Hello PGI compiler!'
#elif IS_XL_FORTRAN_COMPILER
    print *, 'Hello XL compiler!'
#else
    print *, 'Hello unknown compiler - have we met before?'
#endif
end program
在转到Fortran示例之前,我们将先从C++示例开始:
1、在CMakeLists.txt文件中,我们定义了现在熟悉的最小版本、项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-03 LANGUAGES CXX)
2、然后定义可执行目标及其对应的源文件:
add_executable(hello-world hello-world.cpp)
3、然后,通过定义以下目标编译定义,让预处理器了解编译器名称和供应商:
target_compile_definitions(hello-world PUBLIC "COMPILER_NAME=\"${CMAKE_CXX_COMPILER_ID}\"")
if(CMAKE_CXX_COMPILER_ID MATCHES Intel)
    target_compile_definitions(hello-world PUBLIC "IS_INTEL_CXX_COMPILER")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
    target_compile_definitions(hello-world PUBLIC "IS_GNU_CXX_COMPILER")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES PGI)
    target_compile_definitions(hello-world PUBLIC "IS_PGI_CXX_COMPILER")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES XL)
    target_compile_definitions(hello-world PUBLIC "IS_XL_CXX_COMPILER")
endif()
以前的方案已经训练了我们的眼睛,现在我们已经可以预测结果了:
cd build
cmake ..
cmake --build .
./hello-world
如果您使用不同的编译器供应商,则此示例代码将提供不同的问候语。
在前面的示例中的CMakeLists.txt文件中的if语句和前一个方案中似乎重复,作为程序员,我们不喜欢重复自己。我们能更紧凑地表达这个吗?确实我们可以!为此,让我们转来谈谈Fortran示例。
在Fortran示例的CMakeLists.txt文件中,我们需要执行以下操作:
1、我们需要使语言适应Fortran:
project(recipe-03 LANGUAGES Fortran)
2、然后,我们定义可执行文件及其相应的源文件;在这种情况下,大写.F90后缀:
add_executable(hello-world hello-world.F90)
3、然后,通过定义以下目标编译定义,让预处理器非常清楚地了解编译器供应商:
target_compile_definitions(hello-world
    PUBLIC "IS_${CMAKE_Fortran_COMPILER_ID}_FORTRAN_COMPILER"
    )
Fortran示例的剩余行为与C++示例中的相同。
预处理器定义由CMakeLists.txt中的CMake在配置时定义,并传递给预处理器。Fortran示例包含一个非常紧凑的表达式,其中我们使用CMAKE_Fortran_COMPILER_ID变量使用target_compile_definitions构造预处理器定义。为了适应这种情况,我们必须将”Intel“的情况从IS_INTEL_CXX_COMPILER更改为IS_INTEL_FORTRAN_COMPILER。通过使用相应的CMAKE_C_COMPILER_ID和CMAKE_CXX_COMPILER_ID变量,我们可以实现C或C++的相同。但是请注意,CMAKE__COMPILER_ID并不保证为所有编译器或语言定义。

发现主机处理器体系结构
20世纪70年代,64位整数算术的出现和21世纪初个人电脑64位寻址的出现扩大了内存寻址范围,并且投入了大量资源来移植编码32位的代码支持64位寻址的体系结构。许多博客文章(如https://www.viva64.com/en/a/0004/)专门讨论将C++代码移植到64位平台的典型问题和解决方案。非常可取的是,以避免显式硬编码限制的方式进行编程,但您可能处于一种情况,您需要在配置CMake的代码中适应硬编码限制,在此方案中,我们希望讨论检测主机处理器架构的选项。
现在让我们转向CMake一侧。在CMakeLists.txt文件中,我们需要应用以下内容:
1、我们首先定义可执行文件及其源文件依赖项:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-04 LANGUAGES CXX)
add_executable(arch-dependent arch-dependent.cpp)
2、我们检查空指针类型的大小。这在名为CMAKE_SIZEOF_VOID_P的CMake变量中定义,并将告诉我们CPU是32位还是64位。我们通过状态消息让用户了解检测到的大小,并设置预处理器定义:
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
    target_compile_definitions(arch-dependent PUBLIC "IS_64_BIT_ARCH")
    message(STATUS "Target is 64 bits")
else()
    target_compile_definitions(arch-dependent PUBLIC "IS_32_BIT_ARCH")
    message(STATUS "Target is 32 bits")
endif()
3、然后,通过定义以下目标编译定义,让预处理器了解主机处理器体系结构,同时在配置期间打印状态消息:
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i386")
    message(STATUS "i386 architecture detected")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i686")
    message(STATUS "i686 architecture detected")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64")
    message(STATUS "x86_64 architecture detected")
else()
    message(STATUS "host processor architecture is unknown")
endif()
target_compile_definitions(arch-dependent
    PUBLIC "ARCHITECTURE=${CMAKE_HOST_SYSTEM_PROCESSOR}"
    )
4、我们配置项目并注意状态消息(精确消息可能会更改):
mkdir -p build
cd build
cmake ..
5、最后,我们构建并执行代码(实际输出将取决于主机处理器体系结构):
cmake --build .
./arch-dependent
CMake定义CMAKE_HOST_SYSTEM_PROCESSOR变量,以包含当前运行它的处理器的名称。这可以设置为”i386“,”i686“,”x86_64“,”AMD64“等等,当然取决于手头的CPU。CMAKE_SIZEOF_VOID_P被定义为保存指向void类型的指针的大小。我们可以在CMake级别同时查询这两个定义,以便修改目标或目标编译定义。使用预处理器定义,我们可以基于检测到的主机处理器体系结构对源代码编译进行分支编译。如前面的方案中所述,在编写新代码时应避免此类自定义,但有时在使用旧代码或交叉编译时(第13章”替代生成器“和”交叉编译“的主题)非常有用。
除了CMAKE_HOST_SYSTEM_PROCESSOR之外,CMake还定义了CMAKE_SYSTEM_PROCESSOR变量。前者包含当前正在运行的CPU CMake的名称,而后者将包含当前正在构建的CPU的名称。这是一个微妙的差异,在交叉编译时起着非常根本的作用。我们将在第13章”替代生成器和交叉编译“中看到有关交叉编译的更多资料。让CMake检测主机处理器体系结构的替代方法是使用C或C++中定义的符号,并使用CMake的try_run函数生成并尝试执行源代码(请参阅第5章,配置时和构建时操作,Recipe 8,探测执行),由预处理器符号分支。这将返回可在CMake端捕获的已定义好的错误(此策略受https://github.com/axr/solar-cmake/blob/master/TargetArch.cmake启发):
#if defined(__i386) || defined(__i386__) || defined(_M_IX86)
#error cmake_arch i386
#elif defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(_M_X64)
#error cmake_arch x86_64
#endif
此策略也是用于检测目标处理器体系结构的推荐策略,CMake似乎没有提供便携式内在解决方案。还有一种选择。它将只使用CMake,完全不使用预处理器,代价是为每种情况使用不同的源文件,然后使用target_sources CMake命令将其设置为依赖于可执行目标架构的源文件:
add_executable(arch-dependent "")
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i386")
    message(STATUS "i386 architecture detected")
    target_sources(arch-dependent
        PRIVATE
        arch-dependent-i386.cpp
        )
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i686")
    message(STATUS "i686 architecture detected")
    target_sources(arch-dependent
        PRIVATE
        arch-dependent-i686.cpp
        )
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64")
    message(STATUS "x86_64 architecture detected")
    target_sources(arch-dependent
        PRIVATE
        arch-dependent-x86_64.cpp
        )
else()
    message(STATUS "host processor architecture is unknown")
endif()
这种方法显然需要对现有项目进行更多的工作,因为源文件需要分离。此外,不同源文件之间的代码复制肯定会成为一个问题。

发现主机处理器指令集
在此方案中,我们将讨论如何在CMake的帮助下发现主机处理器指令集。此功能最近已添加到CMake中,需要CMake 3.10或更高版本。检测到的主机系统信息可用于设置相应的编译器标志或实现可选的源编译或源代码生成,具体取决于主机系统。在此方案中,我们的目标是检测主机系统信息,使用预处理器定义将其传递给C++源代码,并将信息打印输出。
#include "config.h"
#include
#include
#pragma once
#define NUMBER_OF_LOGICAL_CORES "@_NUMBER_OF_LOGICAL_CORES@"
#define NUMBER_OF_PHYSICAL_CORES "@_NUMBER_OF_PHYSICAL_CORES@"
#define TOTAL_VIRTUAL_MEMORY "@_TOTAL_VIRTUAL_MEMORY@"
#define AVAILABLE_VIRTUAL_MEMORY "@_AVAILABLE_VIRTUAL_MEMORY@"
#define TOTAL_PHYSICAL_MEMORY "@_TOTAL_PHYSICAL_MEMORY@"
#define AVAILABLE_PHYSICAL_MEMORY "@_AVAILABLE_PHYSICAL_MEMORY@"
#define IS_64BIT "@_IS_64BIT@"
#define HAS_FPU "@_HAS_FPU@"
#define HAS_MMX "@_HAS_MMX@"
#define HAS_MMX_PLUS "@_HAS_MMX_PLUS@"
#define HAS_SSE "@_HAS_SSE@"
#define HAS_SSE2 "@_HAS_SSE2@"
#define HAS_SSE_FP "@_HAS_SSE_FP@"
#define HAS_SSE_MMX "@_HAS_SSE_MMX@"
#define HAS_AMD_3DNOW "@_HAS_AMD_3DNOW@"
#define HAS_AMD_3DNOW_PLUS "@_HAS_AMD_3DNOW_PLUS@"
#define HAS_IA64 "@_HAS_IA64@"
#define OS_NAME "@_OS_NAME@"
#define OS_RELEASE "@_OS_RELEASE@"
#define OS_VERSION "@_OS_VERSION@"
#define OS_PLATFORM "@_OS_PLATFORM@"
int main() {
    std::cout << "Number of logical cores: "
          << NUMBER_OF_LOGICAL_CORES << std::endl;
    std::cout << "Number of physical cores: "
          << NUMBER_OF_PHYSICAL_CORES << std::endl;
    std::cout << "Total virtual memory in megabytes: "
          << TOTAL_VIRTUAL_MEMORY << std::endl;
    std::cout << "Available virtual memory in megabytes: "
          << AVAILABLE_VIRTUAL_MEMORY << std::endl;
    std::cout << "Total physical memory in megabytes: "
          << TOTAL_PHYSICAL_MEMORY << std::endl;
    std::cout << "Available physical memory in megabytes: "
          << AVAILABLE_PHYSICAL_MEMORY << std::endl;
    std::cout << "Processor is 64Bit: "
          << IS_64BIT << std::endl;
    std::cout << "Processor has floating point unit: "
          << HAS_FPU << std::endl;
    std::cout << "Processor supports MMX instructions: "
          << HAS_MMX << std::endl;
    std::cout << "Processor supports Ext. MMX instructions: "
          << HAS_MMX_PLUS << std::endl;
    std::cout << "Processor supports SSE instructions: "
          << HAS_SSE << std::endl;
    std::cout << "Processor supports SSE2 instructions: "
          << HAS_SSE2 << std::endl;
    std::cout << "Processor supports SSE FP instructions: "
          << HAS_SSE_FP << std::endl;
    std::cout << "Processor supports SSE MMX instructions: "
          << HAS_SSE_MMX << std::endl;
    std::cout << "Processor supports 3DNow instructions: "
          << HAS_AMD_3DNOW << std::endl; std::cout
        << "Processor supports 3DNow+ instructions: "
        << HAS_AMD_3DNOW_PLUS << std::endl;
    std::cout << "IA64 processor emulating x86 : "
          << HAS_IA64 << std::endl;
    std::cout << "OS name: "
          << OS_NAME << std::endl;
    std::cout << "OS sub-type: "
          << OS_RELEASE << std::endl;
    std::cout << "OS build ID: "
          << OS_VERSION << std::endl;
    std::cout << "OS platform: "
          << OS_PLATFORM << std::endl;
    return EXIT_SUCCESS;
}
我们将使用CMake来填充config.h中的定义,用合理的值填充我们的平台,并将示例源文件编译为可执行文件:
1、首先,我们定义最小CMake版本、项目名称和项目语言:
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(recipe-05 CXX)
2、然后,我们定义目标可执行文件及其源文件和include目录:
add_executable(processor-info "")
target_sources(processor-info
    PRIVATE
    processor-info.cpp
    )
target_include_directories(processor-info
    PRIVATE
    ${PROJECT_BINARY_DIR}
    )
3、然后,我们继续查询主机系统信息以获取一些键:
foreach(key
    IN ITEMS
    NUMBER_OF_LOGICAL_CORES
    NUMBER_OF_PHYSICAL_CORES
    TOTAL_VIRTUAL_MEMORY
    AVAILABLE_VIRTUAL_MEMORY
    TOTAL_PHYSICAL_MEMORY
    AVAILABLE_PHYSICAL_MEMORY
    IS_64BIT
    HAS_FPU
    HAS_MMX
    HAS_MMX_PLUS
    HAS_SSE
    HAS_SSE2
    HAS_SSE_FP
    HAS_SSE_MMX
    HAS_AMD_3DNOW
    HAS_AMD_3DNOW_PLUS
    HAS_IA64
    OS_NAME
    OS_RELEASE
    OS_VERSION
    OS_PLATFORM
    )
    cmake_host_system_information(RESULT _${key} QUERY ${key})
endforeach()
4、定义了相应的变量后,我们配置config.h:
configure_file(config.h.in config.h @ONLY)
5、现在,我们已经准备好配置、构建和测试项目:
mkdir -p build
cd build
cmake ..
cmake --build .
./processor-info
6、当然,输出会因处理器而异。
CMakeLists.txt中的foreach循环查询多个键的值,并定义相应的变量。该方案的核心功能是cmake_host_system_information,它查询运行CMake的主机系统的系统信息。此函数可以在一个函数调用中使用多个键调用,但在这种情况下,我们使用了每个键的一个函数调用。然后,我们使用这些变量在config.h.in中配置占位符并生成config.h 。此配置使用configure_file命令完成。最后,config.h包含在processor-info.cpp 中,一旦编译,它将把值打印到屏幕上。我们将在第5章”配置时和构建时操作“和第6章”生成源代码“中重新讨论此方法。
对于更细粒度的处理器指令集检测,请考虑此模块:https://github.com/VcDevel/Vc/blob/master/cmake/OptimizeForArchitecture.cmake。我们还要指出,有时,生成代码的主机可能与运行代码的主机不同。在登录节点体系结构可能与计算节点上的体系结构不同的计算群集上通常如此。解决此问题的一种方法是将配置和编译作为计算步骤提交,并将其部署到计算节点。

为Eigen库启用矢量化
现代处理器体系结构的向量能力可以显著提高代码的性能。这对于某些类型的运算尤其如此,其中线性代数是最重要的。该方案将显示如何启用矢量化,以加快使用线性代数的特征C++库的简单可执行文件。
#include
#include
#include
EIGEN_DONT_INLINE
double simple_function(Eigen::VectorXd &va, Eigen::VectorXd &vb) {
// this simple function computes the dot product of two vectors // of course it
    could
    be
    expressed
    more
    compactly double d = va.dot(vb);
    return d;
}
int main() {
    int len = 1000000;
    int num_repetitions = 100;
// generate two random vectors Eigen::VectorXd va =
    Eigen::VectorXd::Random(len);
    Eigen::VectorXd vb =
            Eigen::VectorXd::Random(len);
    double result;
    auto start = std::chrono::system_clock::now();
    for (auto i = 0; i
                     < num_repetitions; i++) {
        result = simple_function(va, vb);
    }
    auto end = std::chrono::system_clock::now();
    auto elapsed_seconds = end -
                           start;
    std::cout << "result: " << result << std::endl;
    std::cout << "elapsed seconds: "
              << elapsed_seconds.count() << std::endl;
}
我们期望矢量化能加速simple_function中点积运算的执行。
根据Eigen库的文档,设置适当的编译器标志就足以生成矢量化代码。让我们看看CMakeLists.txt:
1、我们声明一个C++11项目:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-06 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
2、由于我们希望使用Eigen库,因此需要在系统上找到它的头文件:
find_package(Eigen3 3.3 REQUIRED CONFIG)
3、我们包含CheckCXXCompilerFlag.cmake标准模块文件:
include(CheckCXXCompilerFlag)
4、我们检查-march=native编译器标志是否有效:
check_cxx_compiler_flag("-march=native" _march_native_works)
5、还检查了可选的-xHost编译器标志:
check_cxx_compiler_flag("-xHost" _xhost_works)
6、我们设置了一个空变量,即_CXX_FLAGS,来保存我们刚检查过的两个编译器中的一个编译器标志。如果我们看到_march_native_works,我们将_CXX_FLAGS设置为-march=native。如果我们看到xhost_works,我们将_CXX_FLAGS设置为-xHost。如果它们都不起作用,我们将保留_CXX_FLAGS为空,并且矢量化将被禁用:
set(_CXX_FLAGS)
if (_march_native_works)
    message(STATUS "Using processor's vector instructions (-march=native compiler flag set)")
    set(_CXX_FLAGS "-march=native")
elseif (_xhost_works)
    message(STATUS "Using processor's vector instructions (-xHost compiler flag set)")
    set(_CXX_FLAGS "-xHost")
else ()
    message(STATUS "No suitable compiler flag found for vectorization")
endif ()
7、为了进行比较,我们还为未优化的版本定义了一个可执行目标,其中不使用前面的优化标志:
add_executable(linear-algebra-unoptimized linear-algebra.cpp)
target_link_libraries(linear-algebra-unoptimized
    PRIVATE
    Eigen3::Eigen
    )
8、此外,我们还定义了一个优化版本:
add_executable(linear-algebra linear-algebra.cpp)
target_compile_options(linear-algebra
    PRIVATE
    ${_CXX_FLAGS}
    )
target_link_libraries(linear-algebra
    PRIVATE
    Eigen3::Eigen
    )
9、让我们首先比较两个可执行文件(在本例中,-march=native_works):
mkdir -p build
cd build
cmake ..
10、最后,让我们编译和比较计时:
$ cmake --build .
$ ./linear-algebra-unoptimized
result: -261.505
elapsed seconds: 1.97964
$ ./linear-algebra
result: -261.505
elapsed seconds: 1.05048
check_cxx_compiler_flag("-march=native" _march_native_works)此函数接受两个参数:第一个参数是要检查的编译器标志,第二个参数是用于存储检查结果的变量(真或假)。如果检查为正数,我们将工作标志添加到_CXX_FLAGS变量,然后该变量将用于为可执行目标设置编译器标志。
此方案可以与以前的方案结合使用;可以使用cmake_host_system_information查询处理器功能。

检测外部库和程序
在本章中,我们将介绍以下方案:
检测Python解释器
检测Python库
检测Python模块和包
检测BLAS和LAPACK数学库
检测OpenMP并行环境
检测MPI并行环境
检测Eigen库
检测Boost库
检测外部库:1、使用pkg-config
检测外部库:2、编写查找模块
简介
项目通常依赖于其他项目和库。本章演示如何检测外部库、框架和项目,以及如何链接到这些库、框架和项目。例如,CMake有一组相当广泛的预打包模块,用于检测最常用的库和程序,如Python和Boost。您可以使用cmake --help-module-list获得现有模块的列表。但是,并不是所有的库和程序都有涵盖,并且您必须不时地提供自己的检测脚本。在本章中,我们将讨论必要的工具并认识CMake命令的find系列:
find_file以查找命名文件的完整路径
find_library以查找库
find_package以查找和加载外部项目的设置
find_path以查找包含命名文件的目录
find_program以查找程序
Python是一种非常流行的动态语言。许多项目将用Python编写的工具及其主要程序和库打包,或者在配置或构建过程中使用Python脚本。在这种情况下,确保运行时对Python解释器的依赖性也得到满足是很重要的。这个方法将展示如何在配置步骤中检测和使用Python解释器。我们将介绍find_package命令,该命令将在本章中使用。
我们将逐步建立CMakeLists.txt文件:
1、我们首先定义最小CMake版本和项目名称。注意,对于这个示例,我们不需要任何语言支持:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-01 LANGUAGES NONE)
2、然后,我们使用find_package命令来找到Python解释器。
find_package(PythonInterp REQUIRED)
3、然后,我们执行Python命令并捕获其输出和返回值:
execute_process(
    COMMAND
    ${PYTHON_EXECUTABLE} "-c" "print('Hello, world!')"
    RESULT_VARIABLE _status
    OUTPUT_VARIABLE _hello_world
    ERROR_QUIET
    OUTPUT_STRIP_TRAILING_WHITESPACE
)
4、最后,我们打印Python命令的返回值和输出:
message(STATUS "RESULT_VARIABLE is: ${_status}")
message(STATUS "OUTPUT_VARIABLE is: ${_hello_world}")
5、现在,我们可以检查配置步骤的输出:
mkdir -p build
cd build
cmake ..
find_package是用于CMake模块的包装命令,用于发现和设置包。这些模块包含CMake命令,用于标识系统标准位置中的包。CMake模块的文件称为find.cmake,当发出find_package()的调用时,它们包含的命令将在内部运行。
除了在系统上实际发现请求的包之外,find模块还设置了一些有用的变量,反映了实际发现的内容,您可以在自己的CMakeLists.txt中使用这些变量。对于Python解释器,相关模块是findPythonIterp.cmake,它与CMake一起提供,并设置以下变量:
PYTHONINP_FOUND,一个布尔信号,是否找到解释器
PYTHON_EXECUTABLE,Python解释器的可执行文件的路径
PYTHON_VERSION_STRING,Python解释器的完整版本号
PYTHON_VERSION_MAJOR,Python解释器的主要版本号
PYTHON_VERSION_MINOR,Python解释器的次要版本号
PYTHON_VERSION_PATCH,Python解释器的修补程序号
可以强制CMake查找包的特定版本。例如,使用它请求任何版本的大于或等于2.7的Python解释器:find_package(PythonInterp 2.7)
还可以强制满足依赖关系:
find_package(PythonInterp REQUIRED)
在这种情况下,如果在通常的查找位置找不到适合Python解释器的可执行文件,CMake将中止配置。
有时,包没有安装在标准位置,CMake可能无法正确定位它们。可以让CMake查找特定的位置,使用命令行界面开关-D查找特定的软件,以通过相应的选项。对于Python解释器,可以使用以下配置:
cmake -D PYTHON_EXECUTABLE=/custom/location/python ..
这将正确标识非标准/custom/location/python安装目录中的Python可执行文件。
除了检测包之外,我们还想提到一个用于打印变量的方便的助手模块。在这个方案中,我们使用了以下内容:
message(STATUS "RESULT_VARIABLE is: ${_status}")
message(STATUS "OUTPUT_VARIABLE is: ${_hello_world}")
调试的一个方便的替代方法是使用以下内容:
include(CMakePrintHelpers)
cmake_print_variables(_status _hello_world)
这将产生以下输出:
-- _status="0" ; _hello_world="Hello, world!"
有关打印属性和变量的便利宏的更多文档,请参阅https://cmake.org/cmake/help/v3.5/module/CMakePrintHelpers.html。

检测Python库
使用Python工具来分析和操作编译程序的输出是目前广泛使用的。但是,也有其他更强大的方法将诸如Python这样的解释语言与编译语言如C或C++结合起来。一种方法是通过将C类型或C++模块提供给这些类型的新类型和新功能来扩展Python,编译成共享库。这将是第9章混合语言项目中的方案主题。另一种方法是将Python解释器嵌入到C或C++程序中。两种方法都需要以下内容:
Python解释器的工作版本
Python头文件python.h的可用性
Python运行库libpython
所有三个组件必须固定到完全相同的版本。我们已经演示了如何找到Python解释器;在这个方案中,我们将演示如何找到两个缺少的部分,以便成功嵌入。
我们将使用Python嵌入到C程序中的简单示例,该程序可以在Python文档页面上找到。源文件称为hello-embedded-python.c:
#include
int main(int argc, char *argv[]) {
    Py_SetProgramName(argv[0]); /* optional but recommended */
    Py_Initialize();
    PyRun_SimpleString("from time import time,ctime\n"
                       "print 'Today is',ctime(time())\n");
    Py_Finalize();
    return 0;
}
此代码示例将在程序中初始化Python解释器的实例,并使用time Python模块打印日期。
以下是我们的CMakeLists.txt中应遵循的步骤:
1、第一个块包含最小CMake版本、项目名称和所需语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-02 LANGUAGES C)
2、在这个方案中,我们强制使用C99标准。这不是与Python链接的严格要求,但您可能希望具备:
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_C_STANDARD_REQUIRED ON)
3、找到Python解释器。现在这是必需的依赖项:
find_package(PythonInterp REQUIRED)
4、找到Python头和库。相应的模块称为FindPythonLibs.cmake:
find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
5、我们添加一个可执行目标,该目标使用hello-embedded-python.c源文件:
add_executable(hello-embedded-python hello-embedded-python.c)
6、可执行文件包括Python.h头文件。因此,此目标的包含目录必须包括Python包含目录,可从PYTHON_INCLUDE_DIRS变量访问:
target_include_directories(hello-embedded-python
    PRIVATE
    ${PYTHON_INCLUDE_DIRS}
    )
7、最后,我们将可执行文件链接到Python库,可通过PYTHON_LIBRARYES变量进行访问:
target_link_libraries(hello-embedded-python
    PRIVATE
    ${PYTHON_LIBRARIES}
    )
8、现在,我们已准备好运行配置步骤:
mkdir -p build
cd build
cmake ..
9、最后,我们执行生成步骤并运行可执行文件:
cmake --build .
./hello-embedded-python
FindPythonLibs.cmake模块将在标准位置查找Python头和库。由于这些是我们项目所必需的依赖项,如果找不到这些依赖项,配置将停止并出现错误。
注意,我们显式地要求CMake检测Python可执行文件的安装。这是为了确保可执行文件、头文件和库都具有匹配的版本。这对于确保版本之间不存在可能导致运行时崩溃的不匹配至关重要。我们通过使用FindPythonInterp.cmake中定义的PYTHON_VERSION_MAJOR和PYTHON_VERSION_MINOR实现了这一点:
find_package(PythonInterp REQUIRED)
find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
使用EXACT关键字,我们约束CMake来检测特定内容,在这种情况下,Python版本包含文件和库。为了更紧密地匹配,我们可以使用精确的PYTHON_VERSION_STRING:
find_package(PythonInterp REQUIRED)
find_package(PythonLibs ${PYTHON_VERSION_STRING} EXACT REQUIRED)
我们如何确保Python头和库的位置正确,即使它们不在标准安装目录中?对于Python解释器,可以通过-D选项将PYTHON_LIBRARY和PYTHON_INCLUDE_DIR选项传递给命令行界面,从而可以强制CMake查看特定目录。这些选项指定以下内容:
PYTHON_LIBRARY,Python库的路径
PYTHON_INCLUDE_DIR,Python.h所在的路径
这样可以确保获得所需的Python版本。
有时,有必要将-D PYTHON_EXECUTABLE、-D PYTHON_LIBRARY和-D PYTHON_INCLUDE_DIR传递给CMake命令行界面,以便找到所有必要的组件并将其固定到完全相同的版本。
find_package(Python COMPONENTS Interpreter Development REQUIRED)
我们鼓励您阅读以下新模块的文档:
https://cmake.org/cmake/help/v3.12/module/FindPython.html

检测Python模块和包
在前面的方法中,我们演示了如何检测Python解释器,以及如何编译一个简单的C程序,并嵌入Python解释器。这两个任务都是在结合Python和编译语言时让您摆脱基础的基本任务。通常,您的代码将依赖于特定的Python模块,无论是Python工具、嵌入Python的编译程序,还是扩展它的库。例如,NumPy因涉及矩阵代数的问题而在科学界非常流行。在依赖于Python模块或包的项目中,确保满足对这些Python模块的依赖是很重要的。这个方法将展示如何探测用户的环境以找到特定的Python模块和包。
我们将在C++程序中尝试一个稍微涉及更多的嵌入示例。该示例再次从Python在线文档(https://docs.python.org/3.5/extending/embedding.html#pure-embedding)中获取,并展示了如何通过调用编译后的C++可执行文件来执行用户定义的Python模块中的函数。
Python 3示例代码(Py3-pure-embedding.cpp)包含以下源代码(请参阅相应的Python 2等效项https://docs.python.org/2/extending/embedding.html#pure-embedding):
#include
int main(int argc, char *argv[]) {
    PyObject *pName, *pModule, *pDict, *pFunc;
    PyObject *pArgs, *pValue;
    int i;
    if (argc < 3) {
    fprintf(stderr, "Usage: pure-embedding pythonfile funcname [args]\n");
    return 1;
    }
    Py_Initialize();
    PyRun_SimpleString("import sys");
    PyRun_SimpleString("sys.path.append(\".\")");
    pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Error checking of pName left out */
    pModule = PyImport_Import(pName);
    Py_DECREF(pName);
    if (pModule != NULL) {
    pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */
    if (pFunc && PyCallable_Check(pFunc)) {
    pArgs = PyTuple_New(argc - 3);
    for (i = 0; i < argc - 3; ++i) {
    pValue = PyLong_FromLong(atoi(argv[i + 3]));
    if (!pValue) {
        Py_DECREF(pArgs);
        Py_DECREF(pModule);
        fprintf(stderr, "Cannot convert argument\n");
        return 1;
    }
/* pValue reference stolen here: */
    PyTuple_SetItem(pArgs, i, pValue);
    }
    pValue = PyObject_CallObject(pFunc, pArgs);
    Py_DECREF(pArgs);
    if (pValue != NULL) {
    printf("Result of call: %ld\n", PyLong_AsLong(pValue));
    Py_DECREF(pValue);
    } else {
    Py_DECREF(pFunc);
    Py_DECREF(pModule);
    PyErr_Print();
    fprintf(stderr, "Call failed\n");
    return 1;
    }
    } else {
    if (PyErr_Occurred())
    PyErr_Print();
    fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
    }
    Py_XDECREF(pFunc);
    Py_DECREF(pModule);
    } else {
    PyErr_Print();
    fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
    return 1;
    }
    Py_Finalize();
    return 0;
}
我们希望嵌入的Python代码(use_numpy.py)使用NumPy设置矩阵,所有矩阵元素都设置为1.0:
import numpy as np
def print_ones(rows, cols):
A = np.ones(shape=(rows, cols), dtype=float)
print(A)
# we return the number of elements to verify
# that the C++ code is able to receive return values
num_elements = rows*cols
return(num_elements)
在下面的代码中,我们希望能够使用CMake检查NumPy是否可用。我们首先需要确保Python解释器、头文件和库在我们的系统中都是可用的。然后我们将继续确保NumPy可用:
1、首先,我们定义最小的CMake版本、项目名称、语言和C++标准:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-03 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
2、查找解释器、头文件和库与前面的方案完全相同:
find_package(PythonInterp REQUIRED)
find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
3、正确打包的Python模块知道它们的安装位置和版本。这可以通过执行最小的Python脚本来探测。我们可以在CMakeLists.txt中执行此步骤:
execute_process(
    COMMAND
    ${PYTHON_EXECUTABLE} "-c" "import re, numpy; print(re.compile('/__init__.py.*').sub('',numpy.__RESULT_VARIABLE _numpy_status
OUTPUT_VARIABLE _numpy_location
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
)
4、如果找到NumPy,则_numpy_status变量将是一个整数,或者一个字符串,否则带有一些错误消息,而_numpy_location将包含NumPy模块的路径。如果找到NumPy,我们会将其位置保存到一个名为NumPy的新变量中。请注意,新变量已被缓存;这意味着CMake创建一个持久变量,用户可以稍后修改该变量:
if(NOT _numpy_status)
    set(NumPy ${_numpy_location} CACHE STRING "Location of NumPy")
endif()
5、下一步是检查模块的版本。我们再一次在CMakeLists.txt中部署了一些Python“魔法”,将版本号保存到一个名为_numpy_version的变量中:
execute_process(
    COMMAND
    ${PYTHON_EXECUTABLE} "-c" "import numpy; print(numpy.__version__)"
    OUTPUT_VARIABLE _numpy_version
    ERROR_QUIET
    OUTPUT_STRIP_TRAILING_WHITESPACE
)
6、最后,我们让FindPackageHandleStandardArgs CMake包以正确的格式设置NumPy_FOUND变量和输出状态信息:
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(NumPy
    FOUND_VAR NumPy_FOUND
    REQUIRED_VARS NumPy
    VERSION_VAR _numpy_version
    )
7、一旦正确找到所有依赖项,我们就可以编译可执行文件并将其链接到Python库:
add_executable(pure-embedding "")
target_sources(pure-embedding
    PRIVATE
    Py${PYTHON_VERSION_MAJOR}-pure-embedding.cpp
    )
target_include_directories(pure-embedding
    PRIVATE
    ${PYTHON_INCLUDE_DIRS}
    )
target_link_libraries(pure-embedding
    PRIVATE
    ${PYTHON_LIBRARIES}
    )
8、我们还必须确保在build目录中提供use_numpy.py:
add_custom_command(
    OUTPUT
    ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
    COMMAND
    ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/use_numpy.py
    ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
    DEPENDS
    ${CMAKE_CURRENT_SOURCE_DIR}/use_numpy.py
)
# make sure building pure-embedding triggers the above custom command
target_sources(pure-embedding
    PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
    )
9、现在,我们可以测试代码的检测和嵌入:
mkdir -p build
cd build
cmake ..
cmake --build .
./pure-embedding use_numpy print_ones 2 3
此方案中有三个新的CMake命令:execute_process和add_custom_command(始终可用),和find_package_handle_standard_args(需要include(FindPackageHandlesStandardArgs))。
execute_process命令将作为当前发出的CMake命令的子进程执行一个或多个命令。最后一个子进程的返回值将保存到作为参数传递给RESULT_VARIABLE的变量中,而标准输出和标准错误管道的内容将保存到作为参数传递给OUTPUT_VARIABLE和ERROR_VARIABLE的变量中。execute_process允许我们执行任意命令,并使用它们的结果来推断系统的配置。在我们的例子中,我们首先使用它来确保NumPy可用,然后获取模块的版本。
find_package_handle_standard_args命令提供了处理与查找安装在给定系统上的程序和库相关的常见操作的标准工具。在引用此命令时,与版本相关的选项(REQUIRED和EXACT)都得到了正确的处理,无需进一步的CMake代码。其他选项QUITE和COMPONENTS(稍后我们将讨论)也由CMake命令在引擎盖下处理。在这个方案中,我们使用了以下内容:
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(NumPy
    FOUND_VAR NumPy_FOUND
    REQUIRED_VARS NumPy
    VERSION_VAR _numpy_version
    )
当所有必需的变量都设置为有效的文件路径(NumPy)时,该命令将设置变量以指示找到模块(NumPy_FOUND)。它还将把版本设置为传递的版本变量(_numpy_version),并为用户打印出状态消息:
-- Found NumPy: /usr/lib/python3.6/site-packages/numpy (found version "1.14.3")
在本方案中,我们没有进一步使用这些变量。如果NumPy_FOUND返回为FALSE,我们本来可以做的是停止配置。
最后,我们应该对将use_numpy.py复制到生成目录的代码部分进行注释:
add_custom_command(
    OUTPUT
    ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
    COMMAND
    ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/use_numpy.py
    ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
    DEPENDS
    ${CMAKE_CURRENT_SOURCE_DIR}/use_numpy.py
)
target_sources(pure-embedding
    PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
    )
我们本可以使用file (COPY...) 命令实现复制。在这里,我们选择使用add_custom_command命令来确保文件在每次更改时都会被复制,而不仅仅是在第一次运行配置时。我们将在第5章“配置时和构建时操作”中更详细地返回add_custom_command。还要注意target_source,该命令将依赖项添加到${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py;这样做是为了确保生成pure-embedding目标会触发前面的自定义命令。

检测BLAS和LAPACK数学库
许多数值代码严重依赖矩阵和矢量运算。以matrix-vector和matrix-matrix产品为例,方程线性系统的解,对值和值的求解或奇异值分解的计算。这些操作在代码库中可能非常普遍,或者可能必须在如此大的数据集上运行,因此高效实现的可用性在代码中成为绝对必要。幸运的是,有一些库只是为了:基本的线性代数子程序(BLAS)和线性代数包(LAPACK)为涉及线性代数操作的许多任务提供了标准API。不同的供应商提供不同的实现,但它们共享相同的API。虽然数学库基础实现的实际编程语言随时间而变化(Fortran、C、汇编语言),但剩下的历史跟踪是Fortran调用约定。我们在此方案中的任务是针对这些库进行链接,并演示如何无缝地使用用不同语言编写的库,考虑上述调用约定。
为了记录数学库的检测和链接,我们希望编译一个C程序,以矩阵的维数作为命令行输入,生成一个随机平方矩阵A,一个随机向量B,并求解方程的后续线性系统:Ax=b。此外,我们将按随机因子缩放随机矢量b。子程序需要使用BLAS中的DSCAL来执行LAPACK中的标度和DGESV来求解线性方程组的解。C++代码示例的列表包含(linear-algebra.cpp):
#include "CxxBLAS.hpp"
#include "CxxLAPACK.hpp"
#include
#include
#include
int main(int argc, char **argv) {
    if (argc != 2) {
    std::cout << "Usage: ./linear-algebra dim" << std::endl;
    return EXIT_FAILURE;
    }
// Generate a uniform distribution of real number between -1.0 and 1.0
    std::random_device rd;
    std::mt19937 mt(rd());
    std::uniform_real_distribution dist(-1.0, 1.0);
// Allocate matrices and right-hand side vector
    int dim = std::atoi(argv[1]);
    std::vector A(dim * dim);
    std::vector b(dim);
    std::vector ipiv(dim);
// Fill matrix and RHS with random numbers between -1.0 and 1.0
    for (int r = 0; r < dim; r++) {
    for (int c = 0; c < dim; c++) {
    A[r + c * dim] = dist(mt);
    }
    b[r] = dist(mt);
    }
// Scale RHS vector by a random number between -1.0 and 1.0
    C_DSCAL(dim, dist(mt), b.data(), 1);
    std::cout << "C_DSCAL done" << std::endl;
// Save matrix and RHS
    std::vector A1(A);
    std::vector b1(b);
    int info;
    info = C_DGESV(dim, 1, A.data(), dim, ipiv.data(), b.data(), dim);
    std::cout << "C_DGESV done" << std::endl;
    std::cout << "info is " << info << std::endl;
    double eps = 0.0;
    for (int i = 0; i < dim; ++i) {
    double sum = 0.0;
    for (int j = 0; j < dim; ++j)
    sum += A1[i + j * dim] * b[j];
    eps += std::abs(b1[i] - sum);
    }
    std::cout << "check is " << eps << std::endl;
    return 0;
}
我们使用C++11中引入的随机库生成-1.0和1.0之间的随机分布。C_DSCAL和C_DGESV分别是BLAS和LAPACK库的接口,用于处理名称管理,以便从其他编程语言调用这些函数。这在下面的接口文件中与CMake模块结合完成,我们将在下面进一步讨论。
文件CxxBLAS.hpp用extern "C"链接包装BLAS例程:
#pragma once
#include "fc_mangle.h"
#include
#ifdef __cplusplus
extern "C" {
#endif
extern void DSCAL(int *n, double *alpha, double *vec, int *inc);
#ifdef __cplusplus
}
#endif
void C_DSCAL(size_t length, double alpha, double *vec, int inc);
相应的实现文件CxxBLAS.cpp包含:
#include "CxxBLAS.hpp"
#include
// see http://www.netlib.no/netlib/blas/dscal.f
void C_DSCAL(size_t length, double alpha, double *vec, int inc) {
    int big_blocks = (int)(length / INT_MAX);
    int small_size = (int)(length % INT_MAX);
    for (int block = 0; block <= big_blocks; block++) {
    double *vec_s = &vec[block * inc * (size_t)INT_MAX];
    signed int length_s = (block == big_blocks) ? small_size : INT_MAX;
    ::DSCAL(&length_s, &alpha, vec_s, &inc);
    }
}
文件CxxLAPACK.hpp和CxxLAPACK.cpp对LAPACK调用执行相应的转换。
相应的CMakeLists.txt包含以下构建基块:
1、我们定义最低 CMake 版本、项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-04 LANGUAGES CXX C Fortran)
2、我们需要C++11标准:
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
3、此外,我们验证Fortran和C/C++编译器是否协同工作,并生成头文件,该文件将处理名称处理。这两个函数均由FortranCInterface模块提供:
include(FortranCInterface)
FortranCInterface_VERIFY(CXX)
FortranCInterface_HEADER(
    fc_mangle.h
    MACRO_NAMESPACE "FC_"
    SYMBOLS DSCAL DGESV
)
4、然后,我们要求CMake找到BLAS和LAPACK。这些是必需的依赖项:
find_package(BLAS REQUIRED)
find_package(LAPACK REQUIRED)
5、接下来,我们添加一个库,包含BLAS和LAPACK封装的源代码,并链接到LAPACK_LIBRARIES,这也带来了BLAS_LIBRARYES:
add_library(math "")
target_sources(math
    PRIVATE
    CxxBLAS.cpp
    CxxLAPACK.cpp
    )
target_include_directories(math
    PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
    ${CMAKE_CURRENT_BINARY_DIR}
    )
target_link_libraries(math
    PUBLIC
    ${LAPACK_LIBRARIES}
    )
6、请注意,此目标的包含目录和链接库声明为PUBLIC,因此,根据数学库的不同,任何其他目标也将在其包含目录中设置这些目录。
7、最后,我们添加一个可执行的目标和指向math的链接:
add_executable(linear-algebra "")
target_sources(linear-algebra
    PRIVATE
    linear-algebra.cpp
    )
target_link_libraries(linear-algebra
    PRIVATE
    math
    )
8、在配置步骤中,我们可以关注相关输出:
mkdir -p build
cd build
cmake ..
9、最后,我们构建并测试可执行文件:
FindBLAS.cmake和FindLAPACK.cmake将在提供标准BLAS和LAPACK API的库的标准位置进行查找。对于前者,该模块将查找SGEMM函数的Fortran实现,用于常规矩阵的单精度矩阵产品。对于后者,模块搜索CHEEV函数的Fortran实现,以计算复杂赫米提亚矩阵的机位值和功能向量。这些查找通过在内部编译调用这些函数的小型程序进行,并尝试链接到候选库。如果失败,则表示系统中没有兼容的库。
每个编译器在生成机器代码时执行符号的名称操作,不幸的是,此操作的约定不是通用的,而是依赖于编译器的。为了克服这一困难,我们使用FortranCInterface模块(https://cmake.org/cmake/help/v3.5/module/FortranCInterface.html)来验证Fortran和C/C++编译器是否协同工作,并生成与相关编译器兼容的Fortran-C接口头fc_mangle.h。生成的fc_mangle.h必须包含在接口头文件CxxBLAS.hpp和CxxLAPACK.hpp中。为了使用FortranCInterface,我们不得不将C和Fortran支持添加到LANGUAGE列表中。当然,我们可以定义自己的预处理器定义,但是代价是有限的可移植性。
我们将在第9章“混合语言项目”中更详细地讨论Fortran和C的互操作性。
许多数值代码严重依赖矩阵代数运算,正确链接到BLAS和LAPACK API的高性能实现非常重要。供应商为不同的体系结构和并行环境打包其库的方式存在很大差异。FindBLAS.cmake和FindLAPACK.cmake很可能在所有可能的情况下都找不到现有库。如果发生这种情况,您可以通过-D选项从命令行界面显式设置库。

检测OpenMP并行环境
今天,市场上基本上任何一台计算机都是多核计算机,对于注重性能的程序,我们可能需要关注这些多核CPU,并在编程模型中使用并发性。OpenMP是多核CPU上共享内存并行性的标准。为了从OpenMP并行化中获益,通常不需要从根本上修改或重写现有程序。一旦确定了代码中的性能关键部分(例如,使用分析工具,程序员可以添加预处理指令,指示编译器为这些区域生成并行代码)。
在此方案中,我们将演示如何编译包含OpenMP指令的程序,前提是我们使用OpenMP感知编译器。有许多Fortran、C和C++编译器,这些编译器可以利用OpenMP并行性。CMake为OpenMP与C、C++或Fortran结合使用,为较新版本的CMake提供了非常好的支持。此方案将向您展示如何使用简单C++和Fortran程序的导入目标检测和链接到OpenMP,当使用CMake 3.9或以上版本时。
#include
#include
#include
int main(int argc, char *argv[]) {
    std::cout << "number of available processors: " << omp_get_num_procs() << std::endl;
    std::cout << "number of threads: " << omp_get_max_threads() << std::endl;
    auto n = std::stol(argv[1]);
    std::cout << "we will form sum of numbers from 1 to " << n << std::endl;
    // start timer
    auto t0 = omp_get_wtime();
    auto s = 0LL;
#pragma omp parallel for reduction(+ : s)
    for (auto i = 1; i <= n; i++) {
    s += i;
    }
    // stop timer
    auto t1 = omp_get_wtime();
    std::cout << "sum: " << s << std::endl;
    std::cout << "elapsed wall clock time: " << t1 - t0 << " seconds" << std::endl;
    return 0;
}

program example
    use omp_lib
    implicit none
    integer(8) :: i, n, s
    character(len = 32) :: arg
    real(8) :: t0, t1
    print *, "number of available processors:", omp_get_num_procs()
    print *, "number of threads:", omp_get_max_threads()
    call get_command_argument(1, arg)
    read(arg, *) n
    print *, "we will form sum of numbers from 1 to", n
    ! start timer
    t0 = omp_get_wtime()
    s = 0
    ! $omp parallel do reduction(+:s)
    do i = 1, n
    s = s + i
    end do
    ! stop timer
    t1 = omp_get_wtime()
    print *, "sum:", s
    print *, "elapsed wall clock time (seconds):", t1 - t0
end program
我们的CMakeLists.txt适用于C++和Fortran示例,将遵循两种语言之间基本相似的模板:
1、 两者都定义了最低CMake版本、项目名称和语言(CXX或Fortran;我们来看看C++版本):
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
project(recipe-05 LANGUAGES CXX)
2、对于C++示例,我们需要C++11标准:
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
3、两者都调用find_package来搜索OpenMP:
find_package(OpenMP REQUIRED)
4、最后,我们定义可执行目标和指向FindOpenMP模块提供的导入目标的链接(在Fortran案例中,我们链接到OpenMP::OpenMP_Fortran):
add_executable(example example.cpp)
target_link_libraries(example
    PUBLIC
    OpenMP::OpenMP_CXX
    )
5、现在,我们可以配置并构建代码:
mkdir -p build
cd build
cmake ..
cmake --build .
6、让我们首先并行测试它(在本例中,使用四个核心):
./example 1000000000
7、为了进行比较,我们可以在OpenMP线程数设置为1的情况下重新运行该示例:
env OMP_NUM_THREADS=1 ./example 1000000000
我们的简单示例似乎可以工作:编译和链接的代码,我们在多个内核上运行时观察到速度加快。事实上,加速不是OMP_NUM_THREADS的完美倍数,这不是我们在此方案中关心的问题,因为我们专注于需要OpenMP的项目的CMake方面。我们发现,由于采用相当现代的FindOpenMP模块提供的导入目标,链接到OpenMP非常紧凑:
target_link_libraries(example
    PUBLIC
    OpenMP::OpenMP_CXX
    )
我们不必担心编译标志或包含目录,这些设置和依赖项在库OpenMP::OpenMP_CXX的定义中进行了编码,该定义属于“IMPORTED”类型。正如我们在方案3中所述,构建和链接静态和共享库,在第1章“从简单的可执行文件到库”,IMPORTED库是伪目标,它完全编码了我们项目之外的依赖项的使用要求。要使用OpenMP,需要设置编译器标志、包含目录和链接库。所有这些功能都设置为OpenMP::OpenMP_CXX目标的属性,并且只需使用target_link_libraries即可临时应用于example目标。这使得在我们的CMake脚本中使用库变得极其容易。我们可以使用CMakePrintHelpers.cmake标准模块提供的cmake_print_properties命令打印接口的属性:
include(CMakePrintHelpers)
cmake_print_properties(
    TARGETS
    OpenMP::OpenMP_CXX
    PROPERTIES
    INTERFACE_COMPILE_OPTIONS
    INTERFACE_INCLUDE_DIRECTORIES
    INTERFACE_LINK_LIBRARIES
)
请注意,所有有意思的属性都具有前缀INTERFACE_,因为这些属性的使用要求适用于任何想要接口和使用OpenMP目标的目标。
对于3.9以下版本的CMake,我们将不得不多做一点工作:
add_executable(example example.cpp)
target_compile_options(example
    PUBLIC
    ${OpenMP_CXX_FLAGS}
    )
set_target_properties(example
    PROPERTIES
    LINK_FLAGS ${OpenMP_CXX_FLAGS}
    )
对于低于3.5版本的CMake,我们可能需要为Fortran项目显式定义编译标志。
在此方案中,我们讨论了C++和Fortran,但参数和方法对C项目也有效。

 

转载于:https://my.oschina.net/u/943779/blog/3080077

你可能感兴趣的:(CMake Cookbook精要)