cmake 学习笔记

环境

xz@xiaqiu:~/study/cmake$ uname -a
Linux xiaqiu 5.11.0-37-generic #41~20.04.2-Ubuntu SMP Fri Sep 24 09:06:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
xz@xiaqiu:~/study/cmake$ 

文本编辑器 sublime

1. 下载 cmake 语法高亮包

地址:https://github.com/zyxar/Sublime-CMakeLists.git

2. 放到 Sublime Text 3 插件目录

Sublime 的插件目录可以通过首选项—>浏览插件目录的方式打开。然后将下载的高亮包解压在该目录。

cmake 学习笔记_第1张图片

重启sublime 测试

获取代码

本书的源代码可以在GitHub上找到,网址是 https://github.com/dev-cafe/cmake-cookbook 。开源代码遵循MIT许可:只要原始版权和许可声明包含在软件/源代码的任何副本中,可以以任何方式重用和重新混合代码。许可的全文可以在 https://opensource.org/licenses/MIT 中看到。为了测试源码,需要使用Git获取代码:

  • 主要的GNU/Linux发行版都可以通过包管理器安装Git。也可以从Git项目网站 https://gitscm.com 下载二进制发行版,进行安装。
  • MacOS上,可以使用自制或MacPorts安装Git。
  • Windows上,可以从git项目网站( https://git-scm.com )下载git可执行安装文件。

可以通过github桌面客户端访问这些示例,网址为 https://desktop.github.com 。另一种选择是从 https://github.com/dev-cafe/cmake-cookbook 下载zip文件。安装Git后,可以将远程库克隆到本地计算机,如下所示:

$ git clone https://github.com/dev-cafe/cmake-cookbook.git

这将创建一个名为cmake-cookbook的文件夹。本书内容与源码的章节对应,书中章节的编号和源码的顺序相同。在GNU/Linux、MacOS和Windows上,使用最新的持续集成进行测试。我们会在之后讨论测试的设置。我们用标签v1.0标记了与本书中打印的示例相对应的版本。为了与书中内容对应,可以如下获取此特定版本:

$ git clone --single-branch -b v1.0 https://github.com/dev-cafe/cmake-cookbook.git

我们希望收到Bug修复,并且Github库将继续发展。要获取更新,可以选择库的master分支。

Docker镜像

在Docker中进行环境搭建,无疑是非常方便的(依赖项都已经安装好了)。我们的Docker镜像是基于Ubuntu 18.04的镜像制作,您可以按照官方文档https://docs.docker.com 在您的操作系统上安装Docker。Docker安装好后,您可以下载并运行我们的镜像,然后可以对本书示例进行测试:

$ docker run -it devcafe/cmake-cookbook_ubuntu-18.04
$ git clone https://github.com/dev-cafe/cmake-cookbook.git
$ cd cmake-cookbook
$ pipenv install --three
$ pipenv run python testing/collect_tests.py 'chapter-*/recipe-*'

安装必要的软件

与在Docker中使用不同,另一种选择是直接在主机操作系统上安装依赖项。为此,我们概括了一个工具栈,可以作为示例的基础。您必须安装以下组件:

  1. CMake
  2. 编译器
  3. 自动化构建工具
  4. Python

我们还会详细介绍,如何安装所需的某些依赖项。

获取CMake

本书要使用的CMake最低需要为3.5。只有少数示例,演示了3.5版之后引入的新功能。每个示例都有提示,指出示例代码在哪里可用,以及所需的CMake的最低版本。提示信息如下:

NOTE:这个示例的代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe10 中找到,其中包括一个C示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行了测试。

有些(如果不是大多数)示例仍然适用于较低版本的CMake。但是,我们没有测试过这个。我们认为CMake 3.5是大多数系统和发行版的默认软件,而且升级CMake也没什么难度。

CMake可以以多种方式安装。下载并提取由Kitware维护的二进制发行版,可以在所有平台上运行,下载页面位于 https://cmake.org/download/ 。

大多数GNU/Linux发行版都在包管理器中提供了CMake。然而,在一些发行版中,版本可能比较旧,因此下载由Kitware提供的二进制文件当然是首选。下面的命令将从CMake打包的版本中下载并安装在$HOME/Deps/CMake(根据您的偏好调整此路径)下的CMake 3.5.2:

$ cmake_version="3.5.2"
$ target_path=$HOME/Deps/cmake/${cmake_version}
$ cmake_url="https://cmake.org/files/v${cmake_version%.*}/cmake-$ {cmake_version}-Linux-x86_64.tar.gz"
$ mkdir -p "${target_path}"
$ curl -Ls "${cmake_url}" | tar -xz -C "${target_path}" --strip-components=1
$ export PATH=$HOME/Deps/cmake/${cmake_version}/bin${PATH:+:$PATH}
$ cmake --version

macOS获取最新版本的CMake:

$ brew upgrade cmake

Windows上,可以使用Visual Studio 2017,它提供了CMake支持。Visual Studio 2017的安装记录在第13章,可选生成器和交叉编译,示例技巧1,使用Visual Studio 2017构建CMake项目。或者,可以从 https://www.msys2.org 下载MSYS2安装程序,按照其中给出的说明更新包列表,然后使用包管理器pacman安装CMake。下面的代码正在构建64位版本:

$ pacman -S mingw64/mingw-w64-x86_64-cmake

对于32位版本,请使用以下代码(为了简单起见,我们以后只会提到64位版本):

$ pacman -S mingw64/mingw-w64-i686-cmake

MSYS2的另一个特性是在Windows上提供了一个终端,比较像Unix操作系统上的终端,提供可用的开发环境。

编译器

我们将需要C++、C和Fortran的编译器。编译器的版本需要比较新,因为我们需要在大多数示例中支持最新的语言标准。CMake为来自商业和非商业供应商的许多编译器,提供了非常好的支持。为了让示例始终能够跨平台,并尽可能独立于操作系统,我们使用了开源编译器:

  • GNU/Linux上,GNU编译器集合(GCC)是直接的选择。它是免费的,适用于所有发行版。例如,在Ubuntu上,可以安装以下编译器:
$ sudo apt-get install g++ gcc gfortran
  • 在LLVM家族中,Clang也是C++和C编译器的一个很好的选择:
$ sudo apt-get install clang clang++ gfortran
  • macOS上,XCode附带的LLVM编译器适用于C++和C。我们在macOS测试中使用了GCC的Fortran编译器。GCC编译器必须使用包管理器单独安装:
$ brew install gcc
  • Windows上,可以使用Visual Studio测试C++和C示例。或者,可以使用MSYS2安装程序,MSYS2环境中(对于64位版本)使用以下单个命令安装整个工具链,包括C++、C和Fortran编译器:
$ pacman -S mingw64/mingw-w64-x86_64-toolchain

自动化构建工具

自动化构建工具为示例中的项目提供构建和链接的基础设施,最终会安装和使用什么,很大程度上取决于操作系统:

  • GNU/Linux上,GNU Make(很可能)在安装编译器时自动安装。
  • macOS上,XCode将提供GNU Make。
  • Windows上,Visual Studio提供了完整的基础设施。MSYS2环境中,GNU Make作为mingw64/mingw-w64-x86_64工具链包的一部分,进行安装。

为了获得最大的可移植性,我们尽可能使示例不受这些系统相关细节的影响。这种方法的优点是配置、构建和链接,是每个编译器的固有特性

Ninja是一个不错的自动化构建工具,适用于GNU/Linux、macOS和Windows。Ninja注重速度,特别是增量重构。为GNU/Linux、macOS和Windows预先打包的二进制文件可以在GitHub库中找到,网址是 https://github.com/ninja-build/ninja/releases 。Fortran项目中使用CMake和Ninja需要注意。使用CMake 3.7.2或更高版本是必要的,Kitware还有维护Ninja,相关包可以在 https://github.com/Kitware/ninja/releases 上找到。

在GNU/Linux上,可以使用以下一系列命令安装Ninja:

$ mkdir -p ninja$ ninja_url="https://github.com/Kitware/ninja/releases/download/v1.8.2.g 3bbbe.kitware.dyndep-1.jobserver-1/ninja-1.8.2.g3bbbe.kitware.dyndep-1.jobserver-1_x86_64-linux-gnu.tar.gz"$ curl -Ls ${ninja_url} | tar -xz -C ninja --strip-components=1$ export PATH=$HOME/Deps/ninja${PATH:+:$PATH}

Windows上,使用MSYS2环境(假设是64位版本)执行以下命令:

$ pacman -S mingw64/mingw-w64-x86_64-ninja

NOTE:我们建议阅读这篇文章 http://www.aosabook.org/en/posa/ninja.html ,里面是对NInja编译器的历史和设计的选择,进行启发性的讨论。

Python

本书主要关于CMake,但是其中的一些方法,需要使用Python。因此,也需要对Python进行安装:解释器、头文件和库。Python 2.7的生命周期结束于2020年,因此我们将使用Python 3.5。在Ubuntu 14.04 LTS上(这是Travis CI使用的环境,我们后面会讨论),Python 3.5可以安装如下:

sudo apt-get install python3.5-dev

Windows可使用MSYS2环境,Python安装方法如下(假设是64位版本):

$ pacman -S mingw64/mingw-w64-x86_64-python3$ pacman -S mingw64/mingw-w64-x86_64-python3-pip$ python3 -m pip install pipenv

为了运行已经写好的测试机制,还需要一些特定的Python模块。可以使用包管理器在系统范围内安装这些包,也可以在隔离的环境中安装。建议采用后一种方法:

  • 可以在不影响系统环境的情况下,将安装包进行清理/安装。
  • 可以在没有管理员权限的情况下安装包。
  • 可以降低软件版本和依赖项冲突的风险。
  • 为了复现性,可以更好地控制包的依赖性。

为此,我们准备了一个Pipfile。结合pipfile.lock,可以使用Pipenv( http://pipenv.readthedocs )。创建一个独立的环境,并安装所有包。要为示例库创建此环境,可在库的顶层目录中运行以下命令:

$ pip install --user pip pipenv --upgrade$ pipenv install --python python3.5

执行pipenv shell命令会进入一个命令行环境,其中包含特定版本的Python和可用的包。执行exit将退出当前环境。当然,还可以使用pipenv run在隔离的环境中直接执行命令。或者,可以将库中的requirements.txt文件与Virtualenv( http://docs.pythonguide.org/en/latest/dev/virtualenvs/ )和pip结合使用,以达到相同的效果:

$ virtualenv --python=python3.5 venv$ source venv/bin/activate$ pip install -r requirements.txt

可以使用deactivate命令退出虚拟环境。

另一种选择是使用Conda环境,我们建议安装Miniconda。将把最新的Miniconda安装到GNU/Linux的$HOME/Deps/conda目录(从 https://repo.continuum.io/miniconda/miniconda3-latestlinux-x86_64.sh 下载)或macOS(从 https://repo.continuum.io/miniconda/miniconda3-latestmacosx-x86_64.sh 下载):

$ curl -Ls https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh > miniconda.sh$ bash miniconda.sh -b -p "$HOME"/Deps/conda &> /dev/null$ touch "$HOME"/Deps/conda/conda-meta/pinned$ export PATH=$HOME/Deps/conda/bin${PATH:+:$PATH}$ conda config --set show_channel_urls True$ conda config --set changeps1 no$ conda update --all$ conda clean -tipy

Windows上,可以从 https://repo.continuum.io/miniconda/Miniconda3-latest-Windows-x86_64.exe 下载最新的Miniconda。该软件包可以使用PowerShell安装,如下:

$basedir = $pwd.Path + "\"$filepath = $basedir + "Miniconda3-latest-Windows-x86_64.exe"$Anaconda_loc = "C:\Deps\conda"$args = "/InstallationType=JustMe /AddToPath=0 /RegisterPython=0 /S /D=$Anaconda_loc"Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru$conda_path = $Anaconda_loc + "\Scripts\conda.exe"$args = "config --set show_channel_urls True"Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru$args = "config --set changeps1 no"Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru$args = "update --all"Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru$args = "clean -tipy"Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru

安装了Conda后, Python模块可以按如下方式安装:

$ conda create -n cmake-cookbook python=3.5$ conda activate cmake-cookbook$ conda install --file requirements.txt

执行conda deactivate将退出conda的环境。

依赖软件

有些示例需要额外的依赖,这些软件将在这里介绍。

BLAS和LAPACK

大多数Linux发行版都为BLAS和LAPACK提供包。例如,在Ubuntu 14.04 LTS上,您可以运行以下命令:

$ sudo apt-get install libatlas-dev liblapack-dev liblapacke-dev

macOS上,XCode附带的加速库可以满足我们的需要。Windows使用MSYS2环境,可以按如下方式安装这些库(假设是64位版本):

$ pacman -S mingw64/mingw-w64-x86_64-openblas

或者,可以从GitHub ( https://github.com/referlapack/lapack )下载BLAS和LAPACK的参考实现,并从源代码编译库。商业供应商为平台提供安装程序,安装包中有BLAS和LAPACK相关的API。

消息传递接口(MPI)

MPI有许多商业和非商业实现。这里,安装免费的非商业实现就足够了。在Ubuntu 14.04 LTS上,我们推荐OpenMPI。可使用以下命令安装:

$ sudo apt-get install openmpi-bin libopenmpi-dev

在macOS上,Homebrew发布了MPICH

$ brew install mpich

还可以从 https://www.open-mpi.org/software/ 上获取源代码,编译OpenMPI。 对于Windows,Microsoft MPI可以通过 https://msdn.microsoft.com/en-us/library/bb524831(v=vs.85).aspx 下载安装。

线性代数模板库

一些示例需要线性代数模板库,版本为3.3或更高。如果包管理器不提供Eigen,可以使用在线打包源(http://eigen.tuxfamily.org )安装它。例如,在GNU/Linux和macOS上,可以将Eigen安装到$HOME/Deps/Eigen目录:

$ eigen_version="3.3.4"$ mkdir -p eigen$ curl -Ls http://bitbucket.org/eigen/eigen/get/${eigen_version}.tar.gz | tar -xz -C eigen --strip-components=1$ cd eigen$ cmake -H. -Bbuild_eigen -DCMAKE_INSTALL_PREFIX="$HOME/Deps/eigen" &> /dev/null$ cmake --build build_eigen -- install &> /dev/null

Boost库

Boost库适用于各种操作系统,大多数Linux发行版都通过它们的包管理器提供该库的安装。例如,在Ubuntu 14.04 LTS上,Boost文件系统库、Boost Python库和Boost测试库可以通过以下命令安装:

$ sudo apt-get install libboost-filesystem-dev libboost-python-dev libboost-test-dev

对于macOS, MacPorts和自制程序都为最新版本的Boost提供了安装包。我们在macOS上的测试设置安装Boost如下:

$ brew cask uninstall --force oclint$ brew uninstall --force --ignore-dependencies boost$ brew install boost$ brew install boost-python3

Windows的二进制发行版也可以从Boost网站 http://www.boost.org 下载。或者,可以从 https://www.boost.org 下载源代码,并自己编译Boost库。

交叉编译器

在类Debian/Ubuntu系统上,可以使用以下命令安装交叉编译器:

$ sudo apt-get install gcc-mingw-w64 g++-mingw-w64 gfortran-mingw-w64

在macOS上,使用Brew,可以安装以下交叉编译器:

$ brew install mingw-w64

其他包管理器提供相应的包。使用打包的跨编译器的另一种方法,是使用M交叉环境( https://mxe.cc),并从源代码对其进行构建。

ZeroMQ, pkg-config, UUID和Doxygen

Ubuntu 14.04 LTS上,这些包可以安装如下:

$ sudo apt-get install pkg-config libzmq3-dev doxygen graphviz-dev uuid-dev

macOS上,我们建议使用Brew安装:

$ brew install ossp-uuid pkg-config zeromq doxygen

pkg-config程序和UUID库只在类Unix系统上可用。 Windows上使用MSYS2环境,可以按如下方式安装这些依赖项(假设是64位版本):

$ pacman -S mingw64/mingw-w64-x86_64-zeromq$ pacman -S mingw64/mingw-w64-x86_64-pkg-config$ pacman -S mingw64/mingw-w64-x86_64-doxygen$ pacman -S mingw64/mingw-w64-x86_64-graphviz

Conda的构建和部署

想要使用Conda打包的示例的话,需要MinicondaConda构建和部署工具。Miniconda的安装说明之前已经给出。要在GNU/Linux和macOS上安装Conda构建和部署工具,请运行以下命令:

$ conda install --yes --quiet conda-build anaconda-client jinja2 setuptools$ conda clean -tipsy$ conda info -a

这些工具也可以安装在Windows上:

$conda_path = "C:\Deps\conda\Scripts\conda.exe"$args = "install --yes --quiet conda-build anaconda-client jinja2 setuptools"Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru$args = "clean -tipsy"Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru$args = "info -a"Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru

测试环境

示例在下列持续集成(CI)上进行过测试:

  • Travis( https://travis-ci.org )用于GNU/Linux和macOS
  • Appveyor( https://www.appveyor.com )用于Windows
  • CircleCI ( https://circleci.com )用于附加的GNU/Linux测试和商业编译器

CI服务的配置文件可以在示例库中找到( https://github.com/dev-cafe/cmake-cookbook/ ):

  • Travis的配置文件为travis.yml
  • Appveyor的配置文件为.appveyor.yml
  • CircleCI的配置文件为.circleci/config.yml
  • Travis和Appveyor的其他安装脚本,可以在testing/dependencies文件夹中找到。

NOTE:GNU/Linux系统上,Travis使用CMake 3.5.2和CMake 3.12.1对实例进行测试。macOS系统上用CMake 3.12.1进行测试。Appveyor使用CMake 3.11.3进行测试。Circle使用CMake 3.12.1进行测试。

测试机制是一组Python脚本,包含在testing文件夹中。脚本collect_tests.py将运行测试并报告它们的状态。示例也可以单独测试,也可以批量测试;collect_tests.py接受正则表达式作为命令行输入,例如:

$ pipenv run python testing/collect_tests.py 'chapter-0[1,7]/recipe-0[1,2,5]'

该命令将对第1章和第7章的示例1、2和5进行测试。输出的示例如下:

cmake 学习笔记_第2张图片

要获得更详细的输出,可以设置环境变量VERBOSE_OUTPUT=ON

$ env VERBOSE_OUTPUT=ON pipenv run python testing/collect_tests.py 'chapter-*/recipe-*'

从可执行文件到库

将单个源文件编译为可执行文件

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-01中找到,包含C++、C和Fortran示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

本节示例中,我们将演示如何运行CMake配置和构建一个简单的项目。该项目由单个源文件组成,用于生成可执行文件。我们将用C++讨论这个项目,您在GitHub示例库中可以找到C和Fortran的例子。

准备工作

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

#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版本低于该版本,则会发出致命错误:
# set minimum cmake versioncmake_minimum_required(VERSION 3.5 FATAL_ERROR)
  1. 第二行,声明了项目的名称(recipe-01)和支持的编程语言(CXX代表C++)
# project name and languageproject(recipe-01 LANGUAGES CXX)
  1. 指示CMake创建一个新目标:可执行文件hello-world。这个可执行文件是通过编译和链接源文件hello-world.cpp生成的。CMake将为编译器使用默认设置,并自动选择生成工具:
add_executable(hello-world hello-world.cpp)
  1. 将该文件与源文件hello-world.cpp放在相同的目录中。记住,它只能被命名为CMakeLists.txt
  2. 现在,可以通过创建build目录,在build目录下来配置项目:
xz@xiaqiu:~/study/cmake/study/example1$ lsCMakeLists.txt  hello-world.cppxz@xiaqiu:~/study/cmake/study/example1$ mkdir buildxz@xiaqiu:~/study/cmake/study/example1$ cd build/xz@xiaqiu:~/study/cmake/study/example1/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example1/buildxz@xiaqiu:~/study/cmake/study/example1/build$ 
  1. 如果一切顺利,项目的配置已经在build目录中生成。我们现在可以编译可执行文件:
xz@xiaqiu:~/study/cmake/study/example1/build$ cmake --build .Scanning dependencies of target hello-world[ 50%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o[100%] Linking CXX executable hello-world[100%] Built target hello-worldxz@xiaqiu:~/study/cmake/study/example1/build$

工作原理

示例中,我们使用了一个简单的CMakeLists.txt来构建“Hello world”可执行文件:

# set minimum cmake versioncmake_minimum_required(VERSION 3.5 FATAL_ERROR)# project name and languageproject(recipe-01 LANGUAGES CXX)add_executable(hello-world hello-world.cpp)

NOTE:CMake语言不区分大小写,但是参数区分大小写。

TIPS:CMake中,C++是默认的编程语言。不过,我们还是建议使用*LANGUAGES选项在project*命令中显式地声明项目的语言。

要配置项目并生成构建器,我们必须通过命令行界面(CLI)运行CMake。CMake CLI提供了许多选项,cmake -help将输出以显示列出所有可用选项的完整帮助信息,我们将在书中对这些选项进行更多地了解。正如您将从cmake -help的输出中显示的内容,它们中的大多数选项会让你您访问CMake手册,查看详细信息。通过下列命令生成构建器:

$ mkdir -p build$ cd build$ cmake ..

这里,我们创建了一个目录build(生成构建器的位置),进入build目录,并通过指定CMakeLists.txt的位置(本例中位于父目录中)来调用CMake。可以使用以下命令行来实现相同的效果:

$ cmake -H. -Bbuild

该命令是跨平台的,使用了-H-B为CLI选项。-H表示当前目录中搜索根CMakeLists.txt文件。-Bbuild告诉CMake在一个名为build的目录中生成所有的文件。

NOTEcmake -H. -Bbuild也属于CMake标准使用方式: https://cmake.org/pipermail/cmake-developers/2018-January/030520.html 。不过,我们将在本书中使用传统方法(创建一个构建目录,进入其中,并通过将CMake指向CMakeLists.txt*的位置来配置项目)。

运行cmake命令会输出一系列状态消息,显示配置信息:

xz@xiaqiu:~/study/cmake/study/example1/build$ cmake ..
-- The CXX compiler identification is GNU 9.3.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/xz/study/cmake/study/example1/build

NOTE:在与*CMakeLists.txt相同的目录中执行cmake .**,原则上足以配置一个项目。然而,CMake会将所有生成的文件写到项目的根目录中。这将是一个源代码内构建,通常是不推荐的,因为这会混合源代码和项目的目录树。我们首选的是源外构建。*

CMake是一个构建系统生成器。将描述构建系统(如:Unix Makefile、Ninja、Visual Studio等)应当如何操作才能编译代码。然后,CMake为所选的构建系统生成相应的指令。默认情况下,在GNU/Linux和macOS系统上,CMake使用Unix Makefile生成器。Windows上,Visual Studio是默认的生成器。在下一个示例中,我们将进一步研究生成器,并在第13章中重新讨论生成器。

GNU/Linux上,CMake默认生成Unix Makefile来构建项目:

xz@xiaqiu:~/study/cmake/study/example1/build$ lsCMakeCache.txt  CMakeFiles  cmake_install.cmake  hello-world  Makefilexz@xiaqiu:~/study/cmake/study/example1/build$ 
  • Makefile: make将运行指令来构建项目
  • CMakefile:包含临时文件的目录,CMake用于检测操作系统、编译器等。此外,根据所选的生成器,它还包含特定的文件。
  • cmake_install.cmake:处理安装规则的CMake脚本,在项目安装时使用。
  • CMakeCache.txt:如文件名所示,CMake缓存。CMake在重新运行配置时使用这个文件。

要构建示例项目,我们运行以下命令:

$ cmake --build .

最后,CMake不强制指定构建目录执行名称或位置,我们完全可以把它放在项目路径之外。这样做同样有效:

$ mkdir -p /tmp/someplace$ cd /tmp/someplace$ cmake /path/to/source$ cmake --build .

更多信息

官方文档 https://cmake.org/runningcmake/ 给出了运行CMake的简要概述。由CMake生成的构建系统,即上面给出的示例中的Makefile,将包含为给定项目构建目标文件、可执行文件和库的目标及规则。hello-world可执行文件是在当前示例中的唯一目标,运行以下命令:

xz@xiaqiu:~/study/cmake/study/example1/build$ cmake --build . --target helpThe following are some of the valid targets for this Makefile:... all (the default if no target is provided)... clean... depend... rebuild_cache... edit_cache... hello-world... hello-world.o... hello-world.i... hello-world.sxz@xiaqiu:~/study/cmake/study/example1/build$ 

CMake生成的目标比构建可执行文件的目标要多。可以使用cmake --build . --target 语法,实现如下功能:

  • all(或Visual Studio generator中的ALL_BUILD)是默认目标,将在项目中构建所有目标。
  • clean,删除所有生成的文件。
  • rebuild_cache,将调用CMake为源文件生成依赖(如果有的话)。
  • edit_cache,这个目标允许直接编辑缓存。

对于更复杂的项目,通过测试阶段和安装规则,CMake将生成额外的目标:

  • test(或Visual Studio generator中的RUN_TESTS)将在CTest的帮助下运行测试套件。我们将在第4章中详细讨论测试和CTest。
  • install,将执行项目安装规则。我们将在第10章中讨论安装规则。
  • package,此目标将调用CPack为项目生成可分发的包。打包和CPack将在第11章中讨论。

切换生成器

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-02中找到,其中有一个C++、C和Fortran示例。该配置在CMake 3.5版(或更高版本)下测试没问题,并且已经在GNU/Linux、macOS和Windows上进行了测试。

CMake是一个构建系统生成器,可以使用单个CMakeLists.txt为不同平台上的不同工具集配置项目。您可以在CMakeLists.txt中描述构建系统必须运行的操作,以配置并编译代码。基于这些指令,CMake将为所选的构建系统(Unix Makefile、Ninja、Visual Studio等等)生成相应的指令。我们将在第13章中重新讨论生成器。

准备工作

CMake针对不同平台支持本地构建工具列表。同时支持命令行工具(如Unix Makefile和Ninja)和集成开发环境(IDE)工具。用以下命令,可在平台上找到生成器名单,以及已安装的CMake版本:

$ cmake --help

这个命令的输出,将列出CMake命令行界面上所有的选项,您会找到可用生成器的列表。例如,安装了CMake 3.16.3的GNU/Linux机器上的输出:

GeneratorsThe following generators are available on this platform (* marks default):* Unix Makefiles               = Generates standard UNIX makefiles.  Green Hills MULTI            = Generates Green Hills MULTI files                                 (experimental, work-in-progress).  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.

使用此示例,我们将展示为项目切换生成器是多么EASY

具体实施

我们将重用前一节示例中的hello-world.cppCMakeLists.txt。惟一的区别在使用CMake时,因为现在必须显式地使用命令行方式,用-G切换生成器。

  1. 首先,使用以下步骤配置项目:
xz@xiaqiu:~/study/cmake/study/example1$ lsCMakeLists.txt  hello-world.cppxz@xiaqiu:~/study/cmake/study/example1$ mkdir -p buildxz@xiaqiu:~/study/cmake/study/example1$ cd build/xz@xiaqiu:~/study/cmake/study/example1/build$ cmake -G Ninja ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example1/buildxz@xiaqiu:~/study/cmake/study/example1/build$ lsbuild.ninja  CMakeCache.txt  CMakeFiles  cmake_install.cmake  rules.ninjaxz@xiaqiu:~/study/cmake/study/example1/build$ 
  1. 第二步,构建项目:
xz@xiaqiu:~/study/cmake/study/example1/build$ cmake --build .[2/2] Linking CXX executable hello-worldxz@xiaqiu:~/study/cmake/study/example1/build$ 

如何工作

与前一个配置相比,每一步的输出没什么变化。每个生成器都有自己的文件集,所以编译步骤的输出和构建目录的内容是不同的:

  • build.ninjarules.ninja:包含Ninja的所有的构建语句和构建规则。
  • CMakeCache.txt:CMake会在这个文件中进行缓存,与生成器无关。
  • CMakeFiles:包含由CMake在配置期间生成的临时文件。
  • cmake_install.cmake:CMake脚本处理安装规则,并在安装时使用。

cmake --build .ninja命令封装在一个跨平台的接口中。

构建和链接静态库和动态库

NOTE: 这个示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-03 找到,其中有C++和Fortran示例。该配置在CMake 3.5版(或更高版本)测试有效的,并且已经在GNU/Linux、macOS和Windows上进行了测试。

项目中会有单个源文件构建的多个可执行文件的可能。项目中有多个源文件,通常分布在不同子目录中。这种实践有助于项目的源代码结构,而且支持模块化、代码重用和关注点分离。同时,这种分离可以简化并加速项目的重新编译。本示例中,我们将展示如何将源代码编译到库中,以及如何链接这些库。

准备工作

回看第一个例子,这里并不再为可执行文件提供单个源文件,我们现在将引入一个类,用来包装要打印到屏幕上的消息。更新一下的hello-world.cpp:

#include "Message.hpp"#include <cstdlib>#include <iostream>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类包装了一个字符串,并提供重载过的<<操作,并且包括两个源码文件:Message.hpp头文件与Message.cpp源文件。Message.hpp中的接口包含以下内容:

#pragma once#include <iosfwd>#include <string>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 <iostream>#include <string>std::ostream &Message::printObject(std::ostream &os){	os<<"This is my very nice message: "<<std::endl;	os<<message_;	return os;}

具体实施

这里有两个文件需要编译,所以CMakeLists.txt必须进行修改。本例中,先把它们编译成一个库,而不是直接编译成可执行文件:

  1. 创建目标——静态库。库的名称和源码文件名相同,具体代码如下:
# generate a library from sourcesadd_library(message	STATIC	Message.hpp	Message.cpp)
  1. 创建hello-world可执行文件的目标部分不需要修改:
add_executable(hello-world hello-world.cpp)
  1. 最后,将目标库链接到可执行目标:
target_link_libraries(hello-world message)
  1. 对项目进行配置和构建。库编译完成后,将连接到hello-world可执行文件中:
xz@xiaqiu:~/study/cmake/study/example2$ lsCMakeLists.txt  hello-world.cpp  Message.cpp  Message.hppxz@xiaqiu:~/study/cmake/study/example2$ mkdir -p buildxz@xiaqiu:~/study/cmake/study/example2$ cd build/xz@xiaqiu:~/study/cmake/study/example2/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example2/buildxz@xiaqiu:~/study/cmake/study/example2/build$ cmake --build .xz@xiaqiu:~/study/cmake/study/example2/build$ cmake --build .Scanning dependencies of target message[ 25%] Building CXX object CMakeFiles/message.dir/Message.cpp.o[ 50%] Linking CXX static library libmessage.a[ 50%] Built target messageScanning dependencies of target hello-world[ 75%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o[100%] Linking CXX executable hello-world[100%] Built target hello-worldxz@xiaqiu:~/study/cmake/study/example2/build$ ./hello-world This is my very nice message: Hello, CMake World!This is my very nice message: Goodbye, CMake World!xz@xiaqiu:~/study/cmake/study/example2/build$ lsCMakeCache.txt  CMakeFiles  cmake_install.cmake  hello-world  libmessage.a  Makefilexz@xiaqiu:~/study/cmake/study/example2/build$ 

工作原理

本节引入了两个新命令:

  • add_library(message STATIC Message.hpp Message.cpp):生成必要的构建指令,将指定的源码编译到库中。add_library的第一个参数是目标名。整个CMakeLists.txt中,可使用相同的名称来引用库。生成的库的实际名称将由CMake通过在前面添加前缀lib和适当的扩展名作为后缀来形成。生成库是根据第二个参数(STATICSHARED)和操作系统确定的。
  • target_link_libraries(hello-world message): 将库链接到可执行文件。此命令还确保hello-world可执行文件可以正确地依赖于消息库。因此,在消息库链接到hello-world可执行文件之前,需要完成消息库的构建。

编译成功后,构建目录包含libmessage.a一个静态库(在GNU/Linux上)和hello-world可执行文件。CMake接受其他值作为add_library的第二个参数的有效值,我们来看下本书会用到的值:

  • STATIC:用于创建静态库,即编译文件的打包存档,以便在链接其他目标时使用,例如:可执行文件。
  • SHARED:用于创建动态库,即可以动态链接,并在运行时加载的库。可以在CMakeLists.txt中使用add_library(message SHARED Message.hpp Message.cpp)从静态库切换到动态共享对象(DSO)。
  • OBJECT:可将给定add_library的列表中的源码编译到目标文件,不将它们归档到静态库中,也不能将它们链接到共享对象中。如果需要一次性创建静态库和动态库,那么使用对象库尤其有用。我们将在本示例中演示。
  • MODULE:又为DSO组。与SHARED库不同,它们不链接到项目中的任何目标,不过可以进行动态加载。该参数可以用于构建运行时插件。

CMake还能够生成特殊类型的库,这不会在构建系统中产生输出,但是对于组织目标之间的依赖关系,和构建需求非常有用:

  • IMPORTED:此类库目标表示位于项目外部的库。此类库的主要用途是,对现有依赖项进行构建。因此,IMPORTED库将被视为不可变的。我们将在本书的其他章节演示使用IMPORTED库的示例。参见: https://cmake.org/cmake/help/latest/manual/cmakebuildsystem.7.html#imported-targets
  • INTERFACE:与IMPORTED库类似。不过,该类型库可变,没有位置信息。它主要用于项目之外的目标构建使用。我们将在本章第5节中演示INTERFACE库的示例。参见: 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直接集合了源代码。后面的章节中,我们将使用target_sources汇集源码,特别是在第7章。请参见Craig Scott的这篇精彩博文: https://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/ ,其中有对target_sources命令的具体使用。

更多信息

现在展示OBJECT库的使用,修改CMakeLists.txt,如下:

# set minimum cmake versioncmake_minimum_required(VERSION 3.5 FATAL_ERROR)# project name and languageproject(recipe-01 LANGUAGES CXX)# generate an object library from sourcesadd_library(message-objs		    SHARED		    Message.hpp		    Message.cpp)# this is only needed for older compilers# but doesn't hurt either to have itset_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)

输出

xz@xiaqiu:~/study/cmake/study/example2/build$ cmake --build .Scanning dependencies of target message-objs[ 16%] Building CXX object CMakeFiles/message-objs.dir/Message.cpp.o[ 33%] Linking CXX shared library libmessage-objs.so[ 33%] Built target message-objsScanning dependencies of target message-static[ 50%] Linking CXX static library libmessage-static.a[ 50%] Built target message-staticScanning dependencies of target hello-world[ 66%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o[ 83%] Linking CXX executable hello-world[ 83%] Built target hello-worldScanning dependencies of target message-shared[100%] Linking CXX shared library libmessage-shared.so[100%] Built target message-sharedxz@xiaqiu:~/study/cmake/study/example2/build$ make[ 33%] Built target message-objs[ 50%] Built target message-static[ 83%] Built target hello-world[100%] Built target message-sharedxz@xiaqiu:~/study/cmake/study/example2/build$ lsCMakeCache.txt  cmake_install.cmake  libmessage-objs.so    libmessage-static.aCMakeFiles      hello-world          libmessage-shared.so  Makefilexz@xiaqiu:~/study/cmake/study/example2/build$ 

首先,add_library改为add_library(Message-objs OBJECT Message.hpp Message.cpp)。此外,需要保证编译的目标文件与生成位置无关。可以通过使用set_target_properties命令,设置message-objs目标的相应属性来实现。

NOTE: 可能在某些平台和/或使用较老的编译器上,需要显式地为目标设置*POSITION_INDEPENDENT_CODE*属性。

现在,可以使用这个对象库来获取静态库(message-static)和动态库(message-shared)。要注意引用对象库的生成器表达式语法:$。生成器表达式是CMake在生成时(即配置之后)构造,用于生成特定于配置的构建输出。参见: https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html。我们将在第5章中深入研究生成器表达式。最后,将hello-world可执行文件链接到消息库的静态版本。

是否可以让CMake生成同名的两个库?换句话说,它们都可以被称为message,而不是message-staticmessage-shared吗?我们需要修改这两个目标的属性:

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-01 LANGUAGES CXX)
add_library(message-objs
		    SHARED
		    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
			$
			)

set_target_properties(message-shared 
				      PROPERTIES
				      OUTPUT_NAME "message"
				      )
add_library(message-static
			STATIC
			$
			)

set_target_properties(message-static 
				      PROPERTIES
				      OUTPUT_NAME "message"
				      )

add_executable(hello-world hello-world.cpp)
target_link_libraries(hello-world message-static)

输出

xz@xiaqiu:~/study/cmake/study/example2/build$ make
Scanning dependencies of target message-objs
[ 16%] Building CXX object CMakeFiles/message-objs.dir/Message.cpp.o
[ 33%] Linking CXX shared library libmessage-objs.so
[ 33%] Built target message-objs
Scanning dependencies of target message-static
[ 50%] Linking CXX static library libmessage.a
[ 50%] Built target message-static
Scanning dependencies of target hello-world
[ 66%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o
[ 83%] Linking CXX executable hello-world
[ 83%] Built target hello-world
Scanning dependencies of target message-shared
[100%] Linking CXX shared library libmessage.so
[100%] Built target message-shared
xz@xiaqiu:~/study/cmake/study/example2/build$ ls
CMakeCache.txt  cmake_install.cmake  libmessage.a        libmessage.so
CMakeFiles      hello-world          libmessage-objs.so  Makefile
xz@xiaqiu:~/study/cmake/study/example2/build$ 

我们可以链接到DSO吗?这取决于操作系统和编译器:

  1. GNU/Linux和macOS上,不管选择什么编译器,它都可以工作。
  2. Windows上,不能与Visual Studio兼容,但可以与MinGW和MSYS2兼容。

这是为什么呢?生成好的DSO组需要程序员限制符号的可见性。需要在编译器的帮助下实现,但不同的操作系统和编译器上,约定不同。CMake有一个机制来处理这个问题,我们将在第10章中解释它如何工作。

用条件句控制编译

NOTE:这个示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-04 *找到,其中有一个C++示例。该配置在CMake 3.5版(或更高版本)测试有效的,并且已经在GNU/Linux、macOS和Windows上进行了测试。*目前为止,看到的示例比较简单,CMake执行流是线性的:从一组源文件到单个可执行文件,也可以生成静态库或动态库。为了确保完全控制构建项目、配置、编译和链接所涉及的所有步骤的执行流,CMake提供了自己的语言。本节中,我们将探索条件结构if-else- else-endif的使用。

NOTE: CMake语言相当庞杂,由基本的控制结构、特定于CMake的命令和使用新函数模块化扩展语言的基础设施组成。完整的概览可以在这里找到: https://cmake.org/cmake/help/latest/manual/cmake-language.7.html

具体实施

从与上一个示例的的源代码开始,我们希望能够在不同的两种行为之间进行切换:

  1. Message.hppMessage.cpp构建成一个库(静态或动态),然后将生成库链接到hello-world可执行文件中。
  2. Message.hppMessage.cpphello-world.cpp构建成一个可执行文件,但不生成任何一个库。

让我们来看看如何使用CMakeLists.txt来实现:

  1. 首先,定义最低CMake版本、项目名称和支持的语言:
# set minimum cmake versioncmake_minimum_required(VERSION 3.5 FATAL_ERROR)# project name and languageproject(recipe-04 LANGUAGES CXX)
  1. 我们引入了一个新变量USE_LIBRARY,这是一个逻辑变量,值为OFF。我们还打印了它的值:
# introduce a toggle for using a libraryset(USE_LIBRARY OFF)message(STATUS "Compile source into a library? ${USE_LIBRARY}")
  1. CMake中定义BUILD_SHARED_LIBS全局变量,并设置为OFF。调用add_library并省略第二个参数,将构建一个静态库:
# BUILD_SHARED_LIBS is a global flag offered by CMake# to toggle the behavior of add_libraryset(BUILD_SHARED_LIBS OFF)
  1. 然后,引入一个变量_sources,包括Message.hppMessage.cpp
# list sourceslist(APPEND _sources Message.hpp Message.cpp)
  1. 然后,引入一个基于USE_LIBRARY值的if-else语句。如果逻辑为真,则Message.hppMessage.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()
  1. 我们可以再次使用相同的命令集进行构建。由于USE_LIBRARYOFF, hello-world可执行文件将使用所有源文件来编译。可以通过在GNU/Linux上,运行objdump -x命令进行验证。

输出

xz@xiaqiu:~/study/cmake/study/example2/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Compile source into a library? OFF-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example2/buildxz@xiaqiu:~/study/cmake/study/example2/build$ xz@xiaqiu:~/study/cmake/study/example2/build$ makeScanning dependencies of target hello-world[ 33%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o[ 66%] Building CXX object CMakeFiles/hello-world.dir/Message.cpp.o[100%] Linking CXX executable hello-world[100%] Built target hello-worldxz@xiaqiu:~/study/cmake/study/example2/build$ lsCMakeCache.txt  CMakeFiles  cmake_install.cmake  hello-world  Makefilexz@xiaqiu:~/study/cmake/study/example2/build$ 

工作原理

我们介绍了两个变量:USE_LIBRARYBUILD_SHARED_LIBS。这两个变量都设置为OFF。如CMake语言文档中描述,逻辑真或假可以用多种方式表示:

  • 如果将逻辑变量设置为以下任意一种:1ONYEStrueY或非零数,则逻辑变量为true
  • 如果将逻辑变量设置为以下任意一种:0OFFNOfalseNIGNORE、NOTFOUND、空字符串,或者以-NOTFOUND为后缀,则逻辑变量为false

USE_LIBRARY变量将在第一个和第二个行为之间切换。BUILD_SHARED_LIBS是CMake的一个全局标志。因为CMake内部要查询BUILD_SHARED_LIBS全局变量,所以add_library命令可以在不传递STATIC/SHARED/OBJECT参数的情况下调用;如果为false或未定义,将生成一个静态库。

这个例子说明,可以引入条件来控制CMake中的执行流。但是,当前的设置不允许从外部切换,不需要手动修改CMakeLists.txt。原则上,我们希望能够向用户开放所有设置,这样就可以在不修改构建代码的情况下调整配置,稍后将展示如何做到这一点。

NOTE:else()endif()中的(),可能会让刚开始学习CMake代码的同学感到惊讶。其历史原因是,因为其能够指出指令的作用范围。例如,可以使用if(USE_LIBRARY)…else(USE_LIBRARY)…endif(USE_LIBIRAY)**。这个格式并不唯一,可以根据个人喜好来决定使用哪种格式。

TIPS_sources*变量是一个局部变量,不应该在当前范围之外使用,可以在名称前加下划线。

向用户显示选项

NOTE: 这个示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-05 找到,其中有一个C++示例。该配置在CMake 3.5版(或更高版本)测试有效的,并且已经在GNU/Linux、macOS和Windows上进行了测试。

前面的配置中,我们引入了条件句:通过硬编码的方式给定逻辑变量值。不过,这会影响用户修改这些变量。CMake代码没有向读者传达,该值可以从外部进行修改。推荐在CMakeLists.txt中使用option()命令,以选项的形式显示逻辑开关,用于外部设置,从而切换构建系统的生成行为。本节的示例将向您展示,如何使用这个命令。

具体实施

看一下前面示例中的静态/动态库示例。与其硬编码USE_LIBRARYONOFF,现在为其设置一个默认值,同时也可以从外部进行更改:

  1. 用一个选项替换上一个示例的set(USE_LIBRARY OFF)命令。该选项将修改USE_LIBRARY的值,并设置其默认值为OFF
# expose options to the user
option(USE_LIBRARY "Compile source into a library" OFF)
  1. 现在,可以通过CMake的-DCLI选项,将信息传递给CMake来切换库的行为:
xz@xiaqiu:~/study/cmake/study/example2/build$ cmake -D USE_LIBRARY=OFF ..
-- The CXX compiler identification is GNU 9.3.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Compile source into a library? OFF
-- Configuring done
-- Generating done
-- Build files have been written to: /home/xz/study/cmake/study/example2/build
xz@xiaqiu:~/study/cmake/study/example2/build$ make
Scanning dependencies of target hello-world
[ 33%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o
[ 66%] Building CXX object CMakeFiles/hello-world.dir/Message.cpp.o
[100%] Linking CXX executable hello-world
[100%] Built target hello-world
xz@xiaqiu:~/study/cmake/study/example2/build$ ls
CMakeCache.txt  CMakeFiles  cmake_install.cmake  hello-world  Makefile
xz@xiaqiu:~/study/cmake/study/example2/build$ 
xz@xiaqiu:~/study/cmake/study/example2/build$ cmake -D USE_LIBRARY=ON ..
-- Compile source into a library? ON
-- Configuring done
-- Generating done
-- Build files have been written to: /home/xz/study/cmake/study/example2/build
xz@xiaqiu:~/study/cmake/study/example2/build$ make
Scanning dependencies of target message
[ 25%] Building CXX object CMakeFiles/message.dir/Message.cpp.o
[ 50%] Linking CXX static library libmessage.a
[ 50%] Built target message
Scanning dependencies of target hello-world
[ 75%] Linking CXX executable hello-world
[100%] Built target hello-world
xz@xiaqiu:~/study/cmake/study/example2/build$ ls
CMakeCache.txt  CMakeFiles  cmake_install.cmake  hello-world  libmessage.a  Makefile
xz@xiaqiu:~/study/cmake/study/example2/build$ 

-D开关用于为CMake设置任何类型的变量:逻辑变量、路径等等。

工作原理

option可接受三个参数:

option( “help string” [initial value])

  • 表示该选项的变量的名称。
  • "help string"记录选项的字符串,在CMake的终端或图形用户界面中可见。
  • [initial value]选项的默认值,可以是ONOFF

更多信息

有时选项之间会有依赖的情况。示例中,我们提供生成静态库或动态库的选项。但是,如果没有将USE_LIBRARY逻辑设置为ON,则此选项没有任何意义。CMake提供cmake_dependent_option()命令用来定义依赖于其他选项的选项:

include(CMakeDependentOption)#second option depends on the value of the firstcmake_dependent_option(	MAKE_STATIC_LIBRARY "Compile sources into a static library " OFF	"USE_LIBRARY" ON)#third option dependes on the value of the firstcmake_dependent_option(	MAKE_SHARED_LIBRARY "Compile sources into a shared library" On	"USE_LIBRARY" ON)

如果USE_LIBRARYONMAKE_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/CMakeDependentOption.html

TIPS:手册中的任何模块都可以以命令行的方式使用*cmake --help-module 。例如,cmake --help-module CMakeDependentOption*将打印刚才讨论的模块的手册页(帮助页面)。

指定编译器

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-06 中找到,其中有一个C++/C示例。该配置在CMake 3.5版(或更高版本)下测试没问题,并且已经在GNU/Linux、macOS和Windows上进行了测试。

目前为止,我们还没有过多考虑如何选择编译器。CMake可以根据平台和生成器选择编译器,还能将编译器标志设置为默认值。然而,我们通常控制编译器的选择。在后面的示例中,我们还将考虑构建类型的选择,并展示如何控制编译器标志。

具体实施

如何选择一个特定的编译器?例如,如果想使用Intel或Portland Group编译器怎么办?CMake将语言的编译器存储在CMAKE__COMPILER变量中,其中是受支持的任何一种语言,对于我们的目的是CXXCFortran。用户可以通过以下两种方式之一设置此变量:

  1. 使用CLI中的-D选项,例如:
$ cmake -D CMAKE_CXX_COMPILER=clang++ ..
  1. 通过导出环境变量CXX(C++编译器)、CC(C编译器)和FC(Fortran编译器)。例如,使用这个命令使用clang++作为C++编译器:
$ env CXX=clang++ cmake ..

到目前为止讨论的示例,都可以通过传递适当的选项,配置合适的编译器。

NOTE:CMake了解运行环境,可以通过其CLI的*-D开关或环境变量设置许多选项。前一种机制覆盖后一种机制,但是我们建议使用-D*显式设置选项。显式优于隐式,因为环境变量可能被设置为不适合(当前项目)的值。

我们在这里假设,其他编译器在标准路径中可用,CMake在标准路径中执行查找编译器。如果不是这样,用户将需要将完整的编译器可执行文件或包装器路径传递给CMake。

TIPS:我们建议使用*-D CMAKE__COMPILERCLI选项设置编译器,而不是导出CXXCCFC**。这是确保跨平台并与非POSIX兼容的唯一方法。为了避免变量污染环境,这些变量可能会影响与项目一起构建的外部库环境。*

工作原理

配置时,CMake会进行一系列平台测试,以确定哪些编译器可用,以及它们是否适合当前的项目。一个合适的编译器不仅取决于我们所使用的平台,还取决于我们想要使用的生成器。CMake执行的第一个测试基于项目语言的编译器的名称。例如,cc是一个工作的C编译器,那么它将用作C项目的默认编译器。GNU/Linux上,使用Unix Makefile或Ninja时, GCC家族中的编译器很可能是C++CFortran的默认选择。Microsoft Windows上,将选择Visual Studio中的C++C编译器(前提是Visual Studio是生成器)。如果选择MinGW或MSYS Makefile作为生成器,则默认使用MinGW编译器。

更多信息

我们的平台上的CMake,在哪里可以找到可用的编译器和编译器标志?CMake提供--system-information标志,它将把关于系统的所有信息转储到屏幕或文件中。要查看这个信息,请尝试以下操作:

$ cmake --system-information information.txt

文件中(本例中是information.txt)可以看到CMAKE_CXX_COMPILERCMAKE_C_COMPILERCMAKE_Fortran_COMPILER的默认值,以及默认标志。我们将在下一个示例中看到相关的标志。

CMake提供了额外的变量来与编译器交互:

  • CMAKE__COMPILER_LOADED:如果为项目启用了语言,则将设置为TRUE
  • CMAKE__COMPILER_ID:编译器标识字符串,编译器供应商所特有。例如,GCC用于GNU编译器集合,AppleClang用于macOS上的Clang, MSVC用于Microsoft Visual Studio编译器。注意,不能保证为所有编译器或语言定义此变量。
  • CMAKE_COMPILER_IS_GNU:如果语言是GNU编译器集合的一部分,则将此逻辑变量设置为TRUE。注意变量名的部分遵循GNU约定:C语言为CC, C++语言为CXX, Fortran语言为G77
  • CMAKE__COMPILER_VERSION:此变量包含一个字符串,该字符串给定语言的编译器版本。版本信息在major[.minor[.patch[.tweak]]]中给出。但是,对于CMAKE__COMPILER_ID,不能保证所有编译器或语言都定义了此变量。

我们可以尝试使用不同的编译器,配置下面的示例CMakeLists.txt。这个例子中,我们将使用CMake变量来探索已使用的编译器(及版本):

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
# project name and language
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()

输出

xz@xiaqiu:~/study/cmake/study/example2/build$ cmake ..
-- Is the C++ compiler loaded? 1
-- The C++ compiler ID is: Clang
-- Is the C++ from GNU? 
-- The C++ compiler version is: 10.0.0
-- Is the C compiler loaded? 
-- Configuring done
-- Generating done
-- Build files have been written to: /home/xz/study/cmake/study/example2/build

切换构建类型

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-07 中找到,包含一个C++/C示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

CMake可以配置构建类型,例如:Debug、Release等。配置时,可以为Debug或Release构建设置相关的选项或属性,例如:编译器和链接器标志。控制生成构建系统使用的配置变量是CMAKE_BUILD_TYPE。该变量默认为空,CMake识别的值为:

  1. Debug:用于在没有优化的情况下,使用带有调试符号构建库或可执行文件。
  2. Release:用于构建的优化的库或可执行文件,不包含调试符号。
  3. RelWithDebInfo:用于构建较少的优化库或可执行文件,包含调试符号。
  4. MinSizeRel:用于不增加目标代码大小的优化方式,来构建库或可执行文件。

具体实施

示例中,我们将展示如何为项目设置构建类型:

1.首先,定义最低CMake版本、项目名称和支持的语言:

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
# project name and language
project(recipe-07 LANGUAGES C CXX)

2.然后,设置一个默认的构建类型(本例中是Release),并打印一条消息。要注意的是,该变量被设置为缓存变量,可以通过缓存进行编辑:

# we default to Release build typeif(NOT CMAKE_BUILD_TYPE)	set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)endif()message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
  1. 最后,打印出CMake设置的相应编译标志:
message(STATUS "C flags, Debug configuration: ${CMAKE_C_FLAGS_DEBUG}")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.验证配置的输出:

xz@xiaqiu:~/study/cmake/study/example2/build$ cmake ..-- Build type: Release-- C flags, Debug configuration: -g-- C flags, Release configuration: -O3 -DNDEBUG-- C flags, Release configuration with Debug info: -O2 -g -DNDEBUG-- C flags, minimal Release configuration: -Os -DNDEBUG-- C++ flags, Debug configuration: -- C++ flags, Release configuration: -- C++ flags, Release configuration with Debug info: -- C++ flags, minimal Release configuration: -- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example2/buildxz@xiaqiu:~/study/cmake/study/example2/build$ 

切换构建类型:

xz@xiaqiu:~/study/cmake/study/example2/build$ cmake -D CMAKE_BUILD_TYPE=Debug ..-- Build type: Debug-- C flags, Debug configuration: -g-- C flags, Release configuration: -O3 -DNDEBUG-- C flags, Release configuration with Debug info: -O2 -g -DNDEBUG-- C flags, minimal Release configuration: -Os -DNDEBUG-- C++ flags, Debug configuration: -- C++ flags, Release configuration: -- C++ flags, Release configuration with Debug info: -- C++ flags, minimal Release configuration: -- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example2/buildxz@xiaqiu:~/study/cmake/study/example2/build$ 

工作原理

我们演示了如何设置默认构建类型,以及如何(从命令行)覆盖它。这样,就可以控制项目,是使用优化,还是关闭优化启用调试。我们还看到了不同配置使用了哪些标志,这主要取决于选择的编译器。需要在运行CMake时显式地打印标志,也可以仔细阅读运行CMake --system-information的输出,以了解当前平台、默认编译器和语言的默认组合是什么。下一个示例中,我们将讨论如何为不同的编译器和不同的构建类型,扩展或调整编译器标志。

更多信息

我们展示了变量CMAKE_BUILD_TYPE,如何切换生成构建系统的配置(这个链接中有说明: https://cmake.org/cmake/help/v3.5/variable/CMAKE_BUILD_TYPE.html )。Release和Debug配置中构建项目通常很有用,例如:评估编译器优化级别的效果。对于单配置生成器,如Unix Makefile、MSYS Makefile或Ninja,因为要对项目重新配置,这里需要运行CMake两次。不过,CMake也支持复合配置生成器。这些通常是集成开发环境提供的项目文件,最显著的是Visual Studio和Xcode,它们可以同时处理多个配置。可以使用CMAKE_CONFIGURATION_TYPES变量可以对这些生成器的可用配置类型进行调整,该变量将接受一个值列表(可从这个链接获得文档:https://cmake.org/cmake/help/v3.5/variable/CMAKE_CONFIGURATION_TYPES.html)。

下面是对Visual Studio的CMake调用:

$ mkdir -p build$ cd build$ cmake .. -G"Visual Studio 12 2017 Win64" -D CMAKE_CONFIGURATION_TYPES="Release;Debug"

将为Release和Debug配置生成一个构建树。然后,您可以使--config标志来决定构建这两个中的哪一个:

$ cmake --build . --config Release

NOTE:当使用单配置生成器开发代码时,为Release版和Debug创建单独的构建目录,两者使用相同的源代码。这样,就可以在两者之间切换,而不用重新配置和编译。

设置编译器选项

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-08 中找到,有一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

前面的示例展示了如何探测CMake,从而获得关于编译器的信息,以及如何切换项目中的编译器。后一个任务是控制项目的编译器标志。CMake为调整或扩展编译器标志提供了很大的灵活性,您可以选择下面两种方法:

  • CMake将编译选项视为目标属性。因此,可以根据每个目标设置编译选项,而不需要覆盖CMake默认值。
  • 可以使用-DCLI标志直接修改CMAKE__FLAGS_变量。这将影响项目中的所有目标,并覆盖或扩展CMake默认值。

本示例中,我们将展示这两种方法。

准备工作

编写一个示例程序,计算不同几何形状的面积,computer_area.cpp

#include "geometry_circle.hpp"#include "geometry_polygon.hpp"#include "geometry_rhombus.hpp"#include "geometry_square.hpp"#include <cstdlib>#include <iostream>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;}

函数的各种实现分布在不同的文件中,每个几何形状都有一个头文件和源文件。总共有4个头文件和5个源文件要编译:

xz@xiaqiu:~/study/cmake/study/example$ tree.├── 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.hpp0 directories, 10 filesxz@xiaqiu:~/study/cmake/study/example$ 

我们不会为所有文件提供清单,读者可以参考 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-08 。

具体实施

现在已经有了源代码,我们的目标是配置项目,并使用编译器标示进行实验:

  1. 设置CMake的最低版本:
# set minimum cmake versioncmake_minimum_required(VERSION 3.5.0 FATAL_ERROR)
  1. 声明项目名称和语言:
# project name and languageproject(recipe-08 VERSION 0.1.0 LANGUAGES CXX)
  1. 然后,打印当前编译器标志。CMake将对所有C++目标使用这些:
message("C++ compiler flags: ${CMAKE_CXX_FLAGS}")
  1. 为目标准备了标志列表,其中一些将无法在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    )
  1. 为这个库目标设置了编译选项:
target_compile_options(geometry						PRIVATE						${flags})
  1. 然后,将生成compute-areas可执行文件作为一个目标:
add_executable(compute-areas compute-areas.cpp)
  1. 还为可执行目标设置了编译选项:
target_compile_options(compute-areas						PRIVATE						"-fPIC")

9.最后,将可执行文件链接到geometry库:

target_link_libraries(compute-areas	geometry)

如何工作

本例中,警告标志有-Wall-Wextra-Wpedantic,将这些标示添加到geometry目标的编译选项中; compute-areasgeometry目标都将使用-fPIC标志。编译选项可以添加三个级别的可见性:INTERFACEPUBLICPRIVATE

可见性的含义如下:

  • PRIVATE,编译选项会应用于给定的目标,不会传递给与目标相关的目标。我们的示例中, 即使compute-areas将链接到geometry库,compute-areas也不会继承geometry目标上设置的编译器选项。
  • INTERFACE,给定的编译选项将只应用于指定目标,并传递给与目标相关的目标。
  • PUBLIC,编译选项将应用于指定目标和使用它的目标。

目标属性的可见性CMake的核心,我们将在本书中经常讨论这个话题。以这种方式添加编译选项,不会影响全局CMake变量CMAKE__FLAGS_,并能更细粒度控制在哪些目标上使用哪些选项。我们如何验证,这些标志是否按照我们的意图正确使用呢?或者换句话说,如何确定项目在CMake构建时,实际使用了哪些编译标志?一种方法是,使用CMake将额外的参数传递给本地构建工具。本例中会设置环境变量VERBOSE=1

xz@xiaqiu:~/study/cmake/study/example/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - doneC++ compiler flags: -- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example/buildxz@xiaqiu:~/study/cmake/study/example/build$ cmake --build . -- VERBOSE=1/usr/bin/cmake -S/home/xz/study/cmake/study/example -B/home/xz/study/cmake/study/example/build --check-build-system CMakeFiles/Makefile.cmake 0/usr/bin/cmake -E cmake_progress_start /home/xz/study/cmake/study/example/build/CMakeFiles /home/xz/study/cmake/study/example/build/CMakeFiles/progress.marks/usr/bin/make -f CMakeFiles/Makefile2 allmake[1]: 进入目录“/home/xz/study/cmake/study/example/build”/usr/bin/make -f CMakeFiles/geometry.dir/build.make CMakeFiles/geometry.dir/dependmake[2]: 进入目录“/home/xz/study/cmake/study/example/build”cd /home/xz/study/cmake/study/example/build && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/xz/study/cmake/study/example /home/xz/study/cmake/study/example /home/xz/study/cmake/study/example/build /home/xz/study/cmake/study/example/build /home/xz/study/cmake/study/example/build/CMakeFiles/geometry.dir/DependInfo.cmake --color=Dependee "/home/xz/study/cmake/study/example/build/CMakeFiles/geometry.dir/DependInfo.cmake" is newer than depender "/home/xz/study/cmake/study/example/build/CMakeFiles/geometry.dir/depend.internal".Dependee "/home/xz/study/cmake/study/example/build/CMakeFiles/CMakeDirectoryInformation.cmake" is newer than depender "/home/xz/study/cmake/study/example/build/CMakeFiles/geometry.dir/depend.internal".Scanning dependencies of target geometrymake[2]: 离开目录“/home/xz/study/cmake/study/example/build”/usr/bin/make -f CMakeFiles/geometry.dir/build.make CMakeFiles/geometry.dir/buildmake[2]: 进入目录“/home/xz/study/cmake/study/example/build”[ 14%] Building CXX object CMakeFiles/geometry.dir/geometry_circle.cpp.o/usr/bin/c++    -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_circle.cpp.o -c /home/xz/study/cmake/study/example/geometry_circle.cpp[ 28%] Building CXX object CMakeFiles/geometry.dir/geometry_polygon.cpp.o/usr/bin/c++    -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_polygon.cpp.o -c /home/xz/study/cmake/study/example/geometry_polygon.cpp[ 42%] Building CXX object CMakeFiles/geometry.dir/geometry_rhombus.cpp.o/usr/bin/c++    -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_rhombus.cpp.o -c /home/xz/study/cmake/study/example/geometry_rhombus.cpp[ 57%] Building CXX object CMakeFiles/geometry.dir/geometry_square.cpp.o/usr/bin/c++    -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_square.cpp.o -c /home/xz/study/cmake/study/example/geometry_square.cpp[ 71%] Linking CXX static library libgeometry.a/usr/bin/cmake -P CMakeFiles/geometry.dir/cmake_clean_target.cmake/usr/bin/cmake -E cmake_link_script CMakeFiles/geometry.dir/link.txt --verbose=1/usr/bin/ar qc libgeometry.a  CMakeFiles/geometry.dir/geometry_circle.cpp.o CMakeFiles/geometry.dir/geometry_polygon.cpp.o CMakeFiles/geometry.dir/geometry_rhombus.cpp.o CMakeFiles/geometry.dir/geometry_square.cpp.o/usr/bin/ranlib libgeometry.amake[2]: 离开目录“/home/xz/study/cmake/study/example/build”[ 71%] Built target geometry/usr/bin/make -f CMakeFiles/compute-areas.dir/build.make CMakeFiles/compute-areas.dir/dependmake[2]: 进入目录“/home/xz/study/cmake/study/example/build”cd /home/xz/study/cmake/study/example/build && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/xz/study/cmake/study/example /home/xz/study/cmake/study/example /home/xz/study/cmake/study/example/build /home/xz/study/cmake/study/example/build /home/xz/study/cmake/study/example/build/CMakeFiles/compute-areas.dir/DependInfo.cmake --color=Dependee "/home/xz/study/cmake/study/example/build/CMakeFiles/compute-areas.dir/DependInfo.cmake" is newer than depender "/home/xz/study/cmake/study/example/build/CMakeFiles/compute-areas.dir/depend.internal".Dependee "/home/xz/study/cmake/study/example/build/CMakeFiles/CMakeDirectoryInformation.cmake" is newer than depender "/home/xz/study/cmake/study/example/build/CMakeFiles/compute-areas.dir/depend.internal".Scanning dependencies of target compute-areasmake[2]: 离开目录“/home/xz/study/cmake/study/example/build”/usr/bin/make -f CMakeFiles/compute-areas.dir/build.make CMakeFiles/compute-areas.dir/buildmake[2]: 进入目录“/home/xz/study/cmake/study/example/build”[ 85%] Building CXX object CMakeFiles/compute-areas.dir/compute-areas.cpp.o/usr/bin/c++    -fPIC -o CMakeFiles/compute-areas.dir/compute-areas.cpp.o -c /home/xz/study/cmake/study/example/compute-areas.cpp[100%] Linking CXX executable compute-areas/usr/bin/cmake -E cmake_link_script CMakeFiles/compute-areas.dir/link.txt --verbose=1/usr/bin/c++     CMakeFiles/compute-areas.dir/compute-areas.cpp.o  -o compute-areas  libgeometry.a make[2]: 离开目录“/home/xz/study/cmake/study/example/build”[100%] Built target compute-areasmake[1]: 离开目录“/home/xz/study/cmake/study/example/build”/usr/bin/cmake -E cmake_progress_start /home/xz/study/cmake/study/example/build/CMakeFiles 0xz@xiaqiu:~/study/cmake/study/example/build$ lsCMakeCache.txt  CMakeFiles  cmake_install.cmake  compute-areas  libgeometry.a  Makefilexz@xiaqiu:~/study/cmake/study/example/build$ 

输出确认编译标志,确认指令设置正确。控制编译器标志的第二种方法,不用对CMakeLists.txt进行修改。如果想在这个项目中修改geometrycompute-areas目标的编译器选项,可以使用CMake参数进行配置:

$ cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..

这个命令将编译项目,禁用异常和运行时类型标识(RTTI)。

也可以使用全局标志,可以使用CMakeLists.txt运行以下命令:

$ cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..

这将使用-fno-rtti - fpic - wall - Wextra - wpedantic配置geometry目标,同时使用-fno exception -fno-rtti - fpic配置compute-areas

NOTE:本书中,我们推荐为每个目标设置编译器标志。使用*target_compile_options()*不仅允许对编译选项进行细粒度控制,而且还可以更好地与CMake的更高级特性进行集成。

更多信息

大多数时候,编译器有特性标示。当前的例子只适用于GCCClang;其他供应商的编译器不确定是否会理解(如果不是全部)这些标志。如果项目是真正跨平台,那么这个问题就必须得到解决,有三种方法可以解决这个问题。最典型的方法是将所需编译器标志列表附加到每个配置类型CMake变量CMAKE__FLAGS_。标志确定设置为给定编译器有效的标志,因此将包含在if-endif子句中,用于检查CMAKE__COMPILER_ID变量,例如:

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__COMPILER_ID不能保证为所有编译器都定义。此外,一些标志可能会被弃用,或者在编译器的较晚版本中引入。与CMAKE__COMPILER_ID类似,CMAKE__COMPILER_VERSION变量不能保证为所有语言和供应商都提供定义。尽管检查这些变量的方式非常流行,但我们认为更健壮的替代方法是检查所需的标志集是否与给定的编译器一起工作,这样项目中实际上只使用有效的标志。结合特定于项目的变量、target_compile_options和生成器表达式,会让解决方案变得非常强大。我们将在第7章的第3节中展示,如何使用check-and-set模式。

为语言设定标准

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-09 中找到,包含一个C++和Fortran示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

编程语言有不同的标准,即提供改进的语言版本。启用新标准是通过设置适当的编译器标志来实现的。前面的示例中,我们已经展示了如何为每个目标或全局进行配置。3.1版本中,CMake引入了一个独立于平台和编译器的机制,用于为C++C设置语言标准:为目标设置_STANDARD属性。

准备工作

对于下面的示例,需要一个符合C++14标准或更高版本的C++编译器。此示例代码定义了动物的多态,我们使用std::unique_ptr作为结构中的基类:

std::unique_ptr<Animal> cat = Cat("Simon");std::unique_ptr<Animal> dog	= Dog("Marlowe");

没有为各种子类型显式地使用构造函数,而是使用工厂方法的实现。工厂方法使用C++11的可变参数模板实现。它包含继承层次结构中每个对象的创建函数映射:

typedef std::function<std::unique_ptr<Animal>(const std::string&)> create> CreateAnimal;

基于预先分配的标签来分派它们,创建对象:

std::unique_ptr<Animal> simon = farm.create("CAT","Simon");std::unique_ptr<Animal> marlown = farm.create("DOG","Marlowe");

标签和创建功能在工厂使用前已注册:

Factory<CreateAnimal> farm;farm.subscribe("CAT", [](const std::string & n) { return std::make_unique<Cat>(n); });farm.subscribe("DOG", [](const std::string & n) { return std::make_unique<Dog>(n); });

使用C++11 Lambda函数定义创建函数,使用std::make_unique来避免引入裸指针的操作。这个工厂函数是在C++14中引入。

NOTE:CMake的这一功能是在3.1版中添加的,并且还在更新。CMake的后续版本为*C++*标准的后续版本和不同的编译器,提供了越来越好的支持。我们建议您在文档页面上检查您选择的编译器是否受支持: https://cmake.org/cmake/help/latest/manual/cmake-compile-features.7.html#supported-compiler

具体实施

将逐步构建CMakeLists.txt,并展示如何设置语言标准(本例中是C++14):

  1. 声明最低要求的CMake版本,项目名称和语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)project(recipe-09 LANGUAGES CXX)
  1. 要求在Windows上导出所有库符号:
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
  1. 需要为库添加一个目标,这将编译源代码为一个动态库:
add_library(animals  SHARED    Animal.cpp    Animal.hpp    Cat.cpp    Cat.hpp    Dog.cpp    Dog.hpp    Factory.hpp  )
  1. 现在,为目标设置了CXX_STANDARDCXX_EXTENSIONSCXX_STANDARD_REQUIRED属性。还设置了position_independent ent_code属性,以避免在使用一些编译器构建DSO时出现问题:
set_target_properties(animals  PROPERTIES    CXX_STANDARD 14    CXX_EXTENSIONS OFF    CXX_STANDARD_REQUIRED ON    POSITION_INDEPENDENT_CODE 1)
  1. 然后,为"动物农场"的可执行文件添加一个新目标,并设置它的属性:
add_executable(animal-farm animal-farm.cpp)set_target_properties(animal-farm  PROPERTIES    CXX_STANDARD 14    CXX_EXTENSIONS OFF    CXX_STANDARD_REQUIRED ON  )
  1. 最后,将可执行文件链接到库:
target_link_libraries(animal-farm animals)
  1. 现在,来看看猫和狗都调用了什么:

输出

xz@xiaqiu:~/study/cmake/study/example2/cxx-example/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example2/cxx-example/buildxz@xiaqiu:~/study/cmake/study/example2/cxx-example/build$ makeScanning dependencies of target animals[ 16%] Building CXX object CMakeFiles/animals.dir/Animal.cpp.o[ 33%] Building CXX object CMakeFiles/animals.dir/Cat.cpp.o[ 50%] Building CXX object CMakeFiles/animals.dir/Dog.cpp.o[ 66%] Linking CXX shared library libanimals.so[ 66%] Built target animalsScanning dependencies of target animal-farm[ 83%] Building CXX object CMakeFiles/animal-farm.dir/animal-farm.cpp.o[100%] Linking CXX executable animal-farm[100%] Built target animal-farmxz@xiaqiu:~/study/cmake/study/example2/cxx-example/build$ lsanimal-farm  CMakeCache.txt  CMakeFiles  cmake_install.cmake  libanimals.so  Makefilexz@xiaqiu:~/study/cmake/study/example2/cxx-example/build$ ./animal-farm I'm Simon the cat!I'm Marlowethe dog!xz@xiaqiu:~/study/cmake/study/example2/cxx-example/build$ 

Cat.hpp

#pragma once#include "Animal.hpp"class Cat final : public Animal{public:	Cat(const std::string &n) : Animal(n){}private:	std::string name_impl() const override;};

Cat.cpp

#include "Cat.hpp"std::string Cat::name_impl() const{ return "I'm " + name_ + " the cat!"; }

工厂代码

#include #include #include #include #include /// Macro to be used to signal error conditions#define ERROR(message)\{\	std::ostringstream _err;\	_err << "Fatal error \n"\		 << " In function "<<__func__<<"at line "<<__LINE__<<" of file "\		 << __FILE__<<"\n"\		 < class BaseFactory{private:	typedef std::map CallbackMap;	typedef typename CallbackMap::value_type CallbackPair;	typedef typename CallbackMap::const_iterator CallbackConstIter;protected:	CallbackMap callbacks_;	CallbackConstIter retrieve(const std::string& objID) const	{		if(objID.empty())			ERROR("No object identification string provided to the Factory.");		CallbackConstIter i = callbacks_.find(objID);		if(i == callbacks_.end())			ERROR("The unknown object ID " + objID + " occurred in the Factory");		return i;	}private:	bool registerObject(const std::string &objID, const CreateObject &functor)	{		return callbacks_.insert(CallbackPair(objID,functor)).second;	}	bool unRegisterObject(const std::string& objID)	{		return callbacks_.erase(objID) == 1;	}public:	void subscribe(const std::string &objID, const CreateObject &functor)	{		bool done = this->registerObject(objID,functor);		if(!done)		{			ERROR("Subscription of object ID " + objID + " to factory failed");		}	}	void unsubscribe(const std::string &objID)	{		bool done = this->unRegisterObject(objID);		if(!done)		{			ERROR("unsubscribtion of object ID " + objID + "from factory failed!");				}	}};	}template class Factory final : public detail::BaseFactory{public:	template	typename std::result_of::type create(			const std::string &objID,			ObjectInputArgs... data) const	{		return (this->retrieve(objID)->second)(data...);	}};

工作原理

步骤4和步骤5中,我们为动物和动物农场目标设置了一些属性:

  • CXX_STANDARD会设置我们想要的标准。
  • CXX_EXTENSIONS告诉CMake,只启用ISO C++标准的编译器标志,而不使用特定编译器的扩展。
  • CXX_STANDARD_REQUIRED指定所选标准的版本。如果这个版本不可用,CMake将停止配置并出现错误。当这个属性被设置为OFF时,CMake将寻找下一个标准的最新版本,直到一个合适的标志。这意味着,首先查找C++14,然后是C++11,然后是C++98。(译者注:目前会从C++20C++17开始查找)

NOTE:本书发布时,还没有*Fortran_STANDARD可用,但是可以使用target_compile_options*设置标准,可以参见: https://github.com/devcafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-09

TIPS:如果语言标准是所有目标共享的全局属性,那么可以将*CMAKE__STANDARDCMAKE__EXTENSIONSCMAKE__STANDARD_REQUIRED*变量设置为相应的值。所有目标上的对应属性都将使用这些设置。

更多信息

通过引入编译特性,CMake对语言标准提供了更精细的控制。这些是语言标准引入的特性,比如C++11中的可变参数模板和Lambda表达式,以及C++14中的自动返回类型推断。可以使用target_compile_features()命令要求为特定的目标提供特定的特性,CMake将自动为标准设置正确的编译器标志。也可以让CMake为可选编译器特性,生成兼容头文件。

使用控制流

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-10 中找到,有一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

本章前面的示例中,已经使用过if-else-endif。CMake还提供了创建循环的语言工具:foreach endforeachwhile-endwhile。两者都可以与break结合使用,以便尽早从循环中跳出。本示例将展示如何使用foreach,来循环源文件列表。我们将应用这样的循环,在引入新目标的前提下,来为一组源文件进行优化降级。

准备工作

将重用第8节中的几何示例,目标是通过将一些源代码汇集到一个列表中,从而微调编译器的优化。

具体实施

下面是CMakeLists.txt中要的详细步骤:

  1. 与示例8中一样,指定了CMake的最低版本、项目名称和语言,并声明了几何库目标:
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  )
  1. 使用-O3编译器优化级别编译库,对目标设置一个私有编译器选项:
target_compile_options(geometry  PRIVATE      -O3  )
  1. 然后,生成一个源文件列表,以较低的优化选项进行编译:
list(	APPEND sources_with_lower_optimization	geometry_circle.cpp	geometry_rhombus.cpp)
  1. 循环这些源文件,将它们的优化级别调到-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()
  1. 为了确保设置属性,再次循环并在打印每个源文件的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()
  1. 最后,添加compute-areas可执行目标,并将geometry库连接上去:
add_executable(compute-areas compute-areas.cpp)target_link_libraries(compute-areas geometry)

输出

xz@xiaqiu:~/study/cmake/study/example/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - doneC++ compiler flags: -- Setting source properties using IN LISTS syntax:-- Appending -O2 flag for geometry_circle.cpp-- Appending -O2 flag for geometry_rhombus.cpp-- Querying sources properties using plain syntax:-- Source geometry_circle.cpp has the following extra COMPILE_FLAGS: -O2-- Source geometry_rhombus.cpp has the following extra COMPILE_FLAGS: -O2-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example/buildxz@xiaqiu:~/study/cmake/study/example/build$ makeScanning dependencies of target geometry[ 14%] Building CXX object CMakeFiles/geometry.dir/geometry_circle.cpp.o[ 28%] Building CXX object CMakeFiles/geometry.dir/geometry_polygon.cpp.o[ 42%] Building CXX object CMakeFiles/geometry.dir/geometry_rhombus.cpp.o[ 57%] Building CXX object CMakeFiles/geometry.dir/geometry_square.cpp.o[ 71%] Linking CXX static library libgeometry.a[ 71%] Built target geometryScanning dependencies of target compute-areas[ 85%] Building CXX object CMakeFiles/compute-areas.dir/compute-areas.cpp.o[100%] Linking CXX executable compute-areas[100%] Built target compute-areasxz@xiaqiu:~/study/cmake/study/example/build$ lsCMakeCache.txt  CMakeFiles  cmake_install.cmake  compute-areas  libgeometry.a  Makefilexz@xiaqiu:~/study/cmake/study/example/build$ 
  1. 最后,还使用VERBOSE=1检查构建步骤。将看到-O2标志添加在-O3标志之后,但是最后一个优化级别标志(在本例中是-O2)不同:
$ cmake --build . -- VERBOSE=1

输出

xz@xiaqiu:~/study/cmake/study/example/build$ cmake --build . -- VERBOSE=1/usr/bin/cmake -S/home/xz/study/cmake/study/example -B/home/xz/study/cmake/study/example/build --check-build-system CMakeFiles/Makefile.cmake 0/usr/bin/cmake -E cmake_progress_start /home/xz/study/cmake/study/example/build/CMakeFiles /home/xz/study/cmake/study/example/build/CMakeFiles/progress.marks/usr/bin/make -f CMakeFiles/Makefile2 allmake[1]: 进入目录“/home/xz/study/cmake/study/example/build”/usr/bin/make -f CMakeFiles/geometry.dir/build.make CMakeFiles/geometry.dir/dependmake[2]: 进入目录“/home/xz/study/cmake/study/example/build”cd /home/xz/study/cmake/study/example/build && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/xz/study/cmake/study/example /home/xz/study/cmake/study/example /home/xz/study/cmake/study/example/build /home/xz/study/cmake/study/example/build /home/xz/study/cmake/study/example/build/CMakeFiles/geometry.dir/DependInfo.cmake --color=Dependee "/home/xz/study/cmake/study/example/build/CMakeFiles/geometry.dir/DependInfo.cmake" is newer than depender "/home/xz/study/cmake/study/example/build/CMakeFiles/geometry.dir/depend.internal".Dependee "/home/xz/study/cmake/study/example/build/CMakeFiles/CMakeDirectoryInformation.cmake" is newer than depender "/home/xz/study/cmake/study/example/build/CMakeFiles/geometry.dir/depend.internal".Scanning dependencies of target geometrymake[2]: 离开目录“/home/xz/study/cmake/study/example/build”/usr/bin/make -f CMakeFiles/geometry.dir/build.make CMakeFiles/geometry.dir/buildmake[2]: 进入目录“/home/xz/study/cmake/study/example/build”[ 14%] Building CXX object CMakeFiles/geometry.dir/geometry_circle.cpp.o/usr/bin/c++    -O3 -O2 -o CMakeFiles/geometry.dir/geometry_circle.cpp.o -c /home/xz/study/cmake/study/example/geometry_circle.cpp[ 28%] Building CXX object CMakeFiles/geometry.dir/geometry_polygon.cpp.o/usr/bin/c++    -O3 -o CMakeFiles/geometry.dir/geometry_polygon.cpp.o -c /home/xz/study/cmake/study/example/geometry_polygon.cpp[ 42%] Building CXX object CMakeFiles/geometry.dir/geometry_rhombus.cpp.o/usr/bin/c++    -O3 -O2 -o CMakeFiles/geometry.dir/geometry_rhombus.cpp.o -c /home/xz/study/cmake/study/example/geometry_rhombus.cpp[ 57%] Building CXX object CMakeFiles/geometry.dir/geometry_square.cpp.o/usr/bin/c++    -O3 -o CMakeFiles/geometry.dir/geometry_square.cpp.o -c /home/xz/study/cmake/study/example/geometry_square.cpp[ 71%] Linking CXX static library libgeometry.a/usr/bin/cmake -P CMakeFiles/geometry.dir/cmake_clean_target.cmake/usr/bin/cmake -E cmake_link_script CMakeFiles/geometry.dir/link.txt --verbose=1/usr/bin/ar qc libgeometry.a  CMakeFiles/geometry.dir/geometry_circle.cpp.o CMakeFiles/geometry.dir/geometry_polygon.cpp.o CMakeFiles/geometry.dir/geometry_rhombus.cpp.o CMakeFiles/geometry.dir/geometry_square.cpp.o/usr/bin/ranlib libgeometry.amake[2]: 离开目录“/home/xz/study/cmake/study/example/build”[ 71%] Built target geometry/usr/bin/make -f CMakeFiles/compute-areas.dir/build.make CMakeFiles/compute-areas.dir/dependmake[2]: 进入目录“/home/xz/study/cmake/study/example/build”cd /home/xz/study/cmake/study/example/build && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/xz/study/cmake/study/example /home/xz/study/cmake/study/example /home/xz/study/cmake/study/example/build /home/xz/study/cmake/study/example/build /home/xz/study/cmake/study/example/build/CMakeFiles/compute-areas.dir/DependInfo.cmake --color=Dependee "/home/xz/study/cmake/study/example/build/CMakeFiles/compute-areas.dir/DependInfo.cmake" is newer than depender "/home/xz/study/cmake/study/example/build/CMakeFiles/compute-areas.dir/depend.internal".Dependee "/home/xz/study/cmake/study/example/build/CMakeFiles/CMakeDirectoryInformation.cmake" is newer than depender "/home/xz/study/cmake/study/example/build/CMakeFiles/compute-areas.dir/depend.internal".Scanning dependencies of target compute-areasmake[2]: 离开目录“/home/xz/study/cmake/study/example/build”/usr/bin/make -f CMakeFiles/compute-areas.dir/build.make CMakeFiles/compute-areas.dir/buildmake[2]: 进入目录“/home/xz/study/cmake/study/example/build”[ 85%] Building CXX object CMakeFiles/compute-areas.dir/compute-areas.cpp.o/usr/bin/c++    -fPIC -o CMakeFiles/compute-areas.dir/compute-areas.cpp.o -c /home/xz/study/cmake/study/example/compute-areas.cpp[100%] Linking CXX executable compute-areas/usr/bin/cmake -E cmake_link_script CMakeFiles/compute-areas.dir/link.txt --verbose=1/usr/bin/c++     CMakeFiles/compute-areas.dir/compute-areas.cpp.o  -o compute-areas  libgeometry.a make[2]: 离开目录“/home/xz/study/cmake/study/example/build”[100%] Built target compute-areasmake[1]: 离开目录“/home/xz/study/cmake/study/example/build”/usr/bin/cmake -E cmake_progress_start /home/xz/study/cmake/study/example/build/CMakeFiles 0xz@xiaqiu:~/study/cmake/study/example/build$ 

工作原理

foreach-endforeach语法可用于在变量列表上,表示重复特定任务。本示例中,使用它来操作、设置和获取项目中特定文件的编译器标志。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),检索给定文件所需属性的值,并将其存储在CMakeVAR变量中。

NOTE:CMake中,列表是用分号分隔的字符串组。列表可以由*listset命令创建。例如,set(var a b c d e)list(APPEND a b c d e)都创建了列表a;b;c;d;e**。*

TIPS:为了对一组文件降低优化,将它们收集到一个单独的目标(库)中,并为这个目标显式地设置优化级别,而不是附加一个标志,这样可能会更简洁,不过在本示例中,我们的重点是*foreach-endforeach**。*

更多信息

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章中讨论这个主题。了解处理器指令集也有助于优化特定目标平台的性能。本章会介绍,检测环境的方法,并给出建议。

检测操作系统

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-02/recipe-01 中找到。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

CMake是一组跨平台工具。不过,了解操作系统(OS)上执行配置或构建步骤也很重要。从而与操作系统相关的CMake代码,会根据操作系统启用条件编译,或者在可用或必要时使用特定于编译器的扩展。本示例中,我们将通过一个不需要编译任何源代码的示例,演示如何使用CMake检测操作系统。为了简单起见,我们只考虑配置过程。

具体实施

我们将用一个非常简单的CMakeLists.txt进行演示:

  1. 首先,定义CMake最低版本和项目名称。请注意,语言是NONE:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)project(recipe-01 LANGUAGES NONE)
  1. 然后,根据检测到的操作系统信息打印消息:
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()

测试之前,检查前面的代码块,并考虑相应系统上的具体行为。

现在,测试配置项目:

xz@xiaqiu:~/study/cmake/study/example3/build$ cmake ..-- Configuring on/for Linux-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example3/buildxz@xiaqiu:~/study/cmake/study/example3/build$ 
  1. 关于CMake输出,这里有一行很有趣——在Linux系统上(在其他系统上,输出会不同):

    -- Configuring on/for Linux
    

工作原理

CMake为目标操作系统定义了CMAKE_SYSTEM_NAME,因此不需要使用定制命令、工具或脚本来查询此信息。然后,可以使用此变量的值实现特定于操作系统的条件和解决方案。在具有uname命令的系统上,将此变量设置为uname -s的输出。该变量在macOS上设置为“Darwin”。在Linux和Windows上,它分别计算为“Linux”和“Windows”。我们了解了如何在特定的操作系统上执行特定的CMake代码。当然,应该尽量减少这种定制化行为,以便简化迁移到新平台的过程。

NOTE:为了最小化从一个平台转移到另一个平台时的成本,应该避免直接使用Shell命令,还应该避免显式的路径分隔符(Linux和macOS上的前斜杠和Windows上的后斜杠)。CMake代码中只使用前斜杠作为路径分隔符,CMake将自动将它们转换为所涉及的操作系统环境。

处理与平台相关的源代码

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-02/recipe-02 *中找到,包含一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。*理想情况下,应该避免依赖于平台的源代码,但是有时我们没有选择,特别是当要求配置和编译不是自己编写的代码时。本示例中,将演示如何使用CMake根据操作系统编译源代码。

准备工作

修改hello-world.cpp示例代码,将第1章第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)
  1. 然后,定义可执行文件及其对应的源文件:
add_executable(hello-world hello-world.cpp)
  1. 通过定义以下目标编译定义,让预处理器知道系统名称:
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()

继续之前,先检查前面的表达式,并考虑在不同系统上有哪些行为。

  1. 现在,准备测试它,并配置项目:
xz@xiaqiu:~/study/cmake/study/example4/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example4/buildxz@xiaqiu:~/study/cmake/study/example4/build$ makeScanning dependencies of target hello-world[ 50%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o[100%] Linking CXX executable hello-world[100%] Built target hello-worldxz@xiaqiu:~/study/cmake/study/example4/build$ ./hello-world Hello from Linux!xz@xiaqiu:~/study/cmake/study/example4/build$ 

Windows系统上,将看到来自Windows的Hello。其他操作系统将产生不同的输出。

工作原理

hello-world.cpp示例中,有趣的部分是基于预处理器定义IS_WINDOWSIS_LINUXIS_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中配置时定义,通过使用target_compile_definition在预处理阶段使用。可以不重复if-endif语句,以更紧凑的表达式实现,我们将在下一个示例中演示这种重构方式。也可以把if-endif语句加入到一个if-else-else-endif语句中。这个阶段,可以使用add_definitions(-DIS_LINUX)来设置定义(当然,可以根据平台调整定义),而不是使用target_compile_definition。使用add_definitions的缺点是,会修改编译整个项目的定义,而target_compile_definitions给我们机会,将定义限制于一个特定的目标,以及通过PRIVATE|PUBLIC|INTERFACE限定符,限制这些定义可见性。第1章的第8节,对这些限定符有详细的说明:

  • PRIVATE,编译定义将只应用于给定的目标,而不应用于相关的其他目标。
  • INTERFACE,对给定目标的编译定义将只应用于使用它的目标。
  • PUBLIC,编译定义将应用于给定的目标和使用它的所有其他目标。

处理与编译器相关的源代码

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-02/recipe-03 中找到,包含一个C++和Fortran示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

这个方法与前面的方法类似,我们将使用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?'#endifend program

工作原理

CMakeLists.txt会在配置时,进行预处理定义,并传递给预处理器。Fortran示例包含非常紧凑的表达式,我们使用CMAKE_Fortran_COMPILER_ID变量,通过target_compile_definition使用构造预处理器进行预处理定义。为了适应这种情况,我们必须将"Intel"从IS_INTEL_CXX_COMPILER更改为IS_Intel_FORTRAN_COMPILER。通过使用相应的CMAKE_C_COMPILER_IDCMAKE_CXX_COMPILER_ID变量,我们可以在CC++中实现相同的效果。但是,请注意,CMAKE__COMPILER_ID不能保证为所有编译器或语言都定义。

NOTE:对于应该预处理的*Fortran代码使用.F90后缀,对于不需要预处理的代码使用.f90*后缀。

检测处理器体系结构

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-02/recipe-04 中找到,包含一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

19世纪70年代,出现的64位整数运算和本世纪初出现的用于个人计算机的64位寻址,扩大了内存寻址范围,开发商投入了大量资源来移植为32位体系结构硬编码,以支持64位寻址。许多博客文章,如 https://www.viva64.com/en/a/0004/ ,致力于讨论将C++代码移植到64位平台中的典型问题和解决方案。虽然,避免显式硬编码的方式非常明智,但需要在使用CMake配置的代码中适应硬编码限制。本示例中,我们会来讨论检测主机处理器体系结构的选项。

准备工作

我们以下面的arch-dependent.cpp代码为例:

#include #include #include #define STRINGIFY(x) #x#define TOSTRING(x) STRINGIFY(x)std::string say_hello(){	std::string arch_info(TOSTRING(ARCHITECHTURE));	arch_info += std::string(" architecture ");#ifdef IS_32_BIT_ARCH  return arch_info + std::string("Compiled on a 32 bit host processor.");#elif IS_64_BIT_ARCH  return arch_info + std::string("Compiled on a 64 bit host processor.");#else  return arch_info + std::string("Neither 32 nor 64 bit, puzzling ...");#endif}int main(){    std::cout<<say_hello()<<std::endl;    return EXIT_SUCCESS;}

具体实施

CMakeLists.txt文件中,我们需要以下内容:

  1. 首先,定义可执行文件及其源文件依赖关系:
cmake_minimum_required(VERSION 3.5 FALAT_ERROR)project(recipe-64 LANGUAGES CXX)add_executable(arch-dependent arch-dependent.cpp)
  1. 检查空指针类型的大小。CMake的CMAKE_SIZEOF_VOID_P变量会告诉我们CPU是32位还是64位。我们通过状态消息让用户知道检测到的大小,并设置预处理器定义:
if(CMAKE_SIZEOF_VOID_P EQULA 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()
  1. 通过定义以下目标编译定义,让预处理器了解主机处理器架构,同时在配置过程中打印状态消息:
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}"  )
  1. 配置项目,并注意状态消息(打印出的信息可能会发生变化):

输出

xz@xiaqiu:~/study/cmake/study/example5/build$ cmake ..-- Target is 64 bits-- x86_64 architecture detected-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example5/buildxz@xiaqiu:~/study/cmake/study/example5/build$ makeScanning dependencies of target arch-dependent[ 50%] Building CXX object CMakeFiles/arch-dependent.dir/hello-world.cpp.o[100%] Linking CXX executable arch-dependent[100%] Built target arch-dependentxz@xiaqiu:~/study/cmake/study/example5/build$ ./arch-dependent x86_64 architecture. Compiled on a 64 bit host processor.xz@xiaqiu:~/study/cmake/study/example5/build$ 

工作原理

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检测主机处理器体系结构,是使用CC++中定义的符号,结合CMake的try_run函数,尝试构建执行的源代码(见第5.8节)分支的预处理符号。这将返回已定义错误码,这些错误可以在CMake端捕获(此策略的灵感来自 https://github.com/axr/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
#endi

这种策略也是检测目标处理器体系结构的推荐策略,因为CMake似乎没有提供可移植的内在解决方案。另一种选择,将只使用CMake,完全不使用预处理器,代价是为每种情况设置不同的源文件,然后使用target_source命令将其设置为可执行目标arch-dependent依赖的源文件:

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()

检测处理器指令集

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-02/recipe-05 中找到,包含一个C++示例。该示例在CMake 3.10版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

本示例中,我们将讨论如何在CMake的帮助下检测主机处理器支持的指令集。这个功能是较新版本添加到CMake中的,需要CMake 3.10或更高版本。检测到的主机系统信息,可用于设置相应的编译器标志,或实现可选的源代码编译,或根据主机系统生成源代码。本示例中,我们的目标是检测主机系统信息,使用预处理器定义将其传递给C++源代码,并将信息打印到输出中。

准备工作

我们是C++源码(processor-info.cpp)如下所示:

#include "config.h"#include #include 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;}

其包含config.h头文件,我们将使用config.h.in生成这个文件。config.h.in如下:

#pragma once​#define NUMBER_OF_LOGICAL_CORES @[email protected]#define NUMBER_OF_PHYSICAL_CORES @[email protected]#define TOTAL_VIRTUAL_MEMORY @[email protected]#define AVAILABLE_VIRTUAL_MEMORY @[email protected]#define TOTAL_PHYSICAL_MEMORY @[email protected]#define AVAILABLE_PHYSICAL_MEMORY @[email protected]#define IS_64BIT @[email protected]#define HAS_FPU @[email protected]#define HAS_MMX @[email protected]#define HAS_MMX_PLUS @[email protected]#define HAS_SSE @[email protected]#define HAS_SSE2 @[email protected]#define HAS_SSE_FP @[email protected]#define HAS_SSE_MMX @[email protected]#define HAS_AMD_3DNOW @[email protected]#define HAS_AMD_3DNOW_PLUS @[email protected]#define HAS_IA64 @[email protected]#define OS_NAME "@[email protected]"#define OS_RELEASE "@[email protected]"#define OS_VERSION "@[email protected]"#define OS_PLATFORM "@[email protected]"

如何实施

我们将使用CMake为平台填充config.h中的定义,并将示例源文件编译为可执行文件:

  1. 首先,我们定义了CMake最低版本、项目名称和项目语言:
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)project(recipe-05 CXX)
  1. 然后,定义目标可执行文件及其源文件,并包括目录:
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()
  1. 定义了相应的变量后,配置config.h:
configure_file(config.h.in config.h @ONLY)
  1. 现在准备好配置、构建和测试项目:
xz@xiaqiu:~/study/cmake/study/example6/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example6/buildxz@xiaqiu:~/study/cmake/study/example6/build$ makeScanning dependencies of target processor_info[ 50%] Building CXX object CMakeFiles/processor_info.dir/processor-info.cpp.o[100%] Linking CXX executable processor_info[100%] Built target processor_infoxz@xiaqiu:~/study/cmake/study/example6/build$ lsCMakeCache.txt  CMakeFiles  cmake_install.cmake  config.h  Makefile  processor_infoxz@xiaqiu:~/study/cmake/study/example6/build$ ./processor_info Number of logical cores: 4Number of physical cores: 2Total virtual memory in megabytes: 979Available virtual memory in megabytes: 0Total physical memory in megabytes: 3849Available physical memory in megabytes: 1263Processor is 64Bit: 1Processor has floating point unit: 1Processor supports MMX instructions: 1Processor supports Ext. MMX instructions: 0Processor supports SSE instructions: 1Processor supports SSE2 instructions: 1Processor supports SSE FP instructions: 0Processor supports SSE MMX instructions: 0Processor supports 3DNow instructions: 0Processor supports 3DNow+ instructions: 0IA64 processor emulating x86 : 0OS name: LinuxOS sub-type: 5.11.0-37-genericOS build ID: #41~20.04.2-Ubuntu SMP Fri Sep 24 09:06:38 UTC 2021OS platform: x86_64xz@xiaqiu:~/study/cmake/study/example6/build$ 

工作原理

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库使能向量化

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-02/recipe-06 中找到,包含一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

处理器的向量功能,可以提高代码的性能。对于某些类型的运算来说尤为甚之,例如:线性代数。本示例将展示如何使能矢量化,以便使用线性代数的Eigen C++库加速可执行文件。

准备工作

我们用Eigen C++模板库,用来进行线性代数计算,并展示如何设置编译器标志来启用向量化。这个示例的源代码linear-algebra.cpp文件:

#include #include #include EIGEN_DONT_INLINEdouble simple_function(Eigen::VectorXd &va,Eigen::VectorXd &vb){	  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;}

如何实施

根据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)
  1. 使用Eigen库,我们需要在系统上找到它的头文件:
find_package(Eigen3 3.3 REQUIRED CONFIG)
  1. CheckCXXCompilerFlag.cmake标准模块文件:
include(CheckCXXCompilerFlag)
  1. 检查-march=native编译器标志是否工作:
check_cxx_compiler_flag("-march=native" _march_native_works)
  1. 另一个选项-xHost编译器标志也开启:
check_cxx_compiler_flag("-xHost" _xhost_works)
  1. 设置了一个空变量_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()
  1. 为了便于比较,我们还为未优化的版本定义了一个可执行目标,不使用优化标志:
add_executable(linear-algebra-unoptimized linear-algebra.cpp)target_link_libraries(linear-algebra-unoptimized	PRIVATE	Eigen3::Eigen)
  1. 此外,我们定义了一个优化版本:
add_executable(linear-algebra linear-algebra.cpp)target_compile_options(linear-algebra					PRIVATE					${_CXX_FLAGS})target_link_libraries(linear-algebra					PRIVATE					Eigen3::Eigen)

CMakeLists.txt

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)find_package(Eigen3 3.3 REQUIRED CONFIG)include(CheckCXXCompilerFlag)check_cxx_compiler_flag("-march=native" _march_native_works)check_cxx_compiler_flag("-xHost" _xhost_works)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()add_executable(linear-algebra-unoptimized linear-algebra.cpp)target_link_libraries(linear-algebra-unoptimized	PRIVATE	Eigen3::Eigen)	add_executable(linear-algebra linear-algebra.cpp)target_compile_options(linear-algebra	PRIVATE	${_CXX_FLAGS}	)target_link_libraries(linear-algebra	PRIVATE	Eigen3::Eigen)
  1. 让我们比较一下这两个可执行文件——首先我们配置(在本例中,-march=native_works):
xz@xiaqiu:~/study/cmake/study/example7/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Performing Test _march_native_works-- Performing Test _march_native_works - Success-- Performing Test _xhost_works-- Performing Test _xhost_works - Failed-- Using processor's vector instructions (-march=native compiler flag set)-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example7/buildxz@xiaqiu:~/study/cmake/study/example7/build$ makeScanning dependencies of target linear-algebra[ 25%] Building CXX object CMakeFiles/linear-algebra.dir/linear-algebra.cpp.o[ 50%] Linking CXX executable linear-algebra[ 50%] Built target linear-algebraScanning dependencies of target linear-algebra-unoptimized[ 75%] Building CXX object CMakeFiles/linear-algebra-unoptimized.dir/linear-algebra.cpp.o[100%] Linking CXX executable linear-algebra-unoptimized[100%] Built target linear-algebra-unoptimizedxz@xiaqiu:~/study/cmake/study/example7/build$ lsCMakeCache.txt  cmake_install.cmake  linear-algebra-unoptimizedCMakeFiles      linear-algebra       Makefilexz@xiaqiu:~/study/cmake/study/example7/build$ ./linear-algebra-unoptimized result: -261.505elapsed seconds: 2733515162xz@xiaqiu:~/study/cmake/study/example7/build$ ./linear-algebraresult: -261.505elapsed seconds: 1410079816xz@xiaqiu:~/study/cmake/study/example7/build$ 

工作原理

大多数处理器提供向量指令集,代码可以利用这些特性,获得更高的性能。由于线性代数运算可以从Eigen库中获得很好的加速,所以在使用Eigen库时,就要考虑向量化。我们所要做的就是,指示编译器为我们检查处理器,并为当前体系结构生成本机指令。不同的编译器供应商会使用不同的标志来实现这一点:GNU编译器使用-march=native标志来实现这一点,而Intel编译器使用-xHost标志。使用CheckCXXCompilerFlag.cmake模块提供的check_cxx_compiler_flag函数进行编译器标志的检查:

check_cxx_compiler_flag("-march=native" _march_native_works)

这个函数接受两个参数:

  • 第一个是要检查的编译器标志。
  • 第二个是用来存储检查结果(true或false)的变量。如果检查为真,我们将工作标志添加到_CXX_FLAGS变量中,该变量将用于为可执行目标设置编译器标志。

检测外部库和程序

本章中主要内容有:

  • 检测Python解释器
  • 检测Python库
  • 检测Python模块和包
  • 检测BLAS和LAPACK数学库
  • 检测OpenMP并行环境
  • 检测MPI并行环境
  • 检测Eigen库
  • 检测Boost库
  • 检测外部库:Ⅰ. 使用pkg-config
  • 检测外部库:Ⅱ. 书写find模块

我们的项目常常会依赖于其他项目和库。本章将演示,如何检测外部库、框架和项目,以及如何链接到这些库。CMake有一组预打包模块,用于检测常用库和程序,例如:Python和Boost。可以使用cmake --help-module-list获得现有模块的列表。但是,不是所有的库和程序都包含在其中,有时必须自己编写检测脚本。本章将讨论相应的工具,了解CMake的find族命令:

  • find_file:在相应路径下查找命名文件
  • find_library:查找一个库文件
  • find_package:从外部项目查找和加载设置
  • find_path:查找包含指定文件的目录
  • find_program:找到一个可执行程序

检测Python解释器

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-01 中找到。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

Python是一种非常流行的语言。许多项目用Python编写的工具,从而将主程序和库打包在一起,或者在配置或构建过程中使用Python脚本。这种情况下,确保运行时对Python解释器的依赖也需要得到满足。本示例将展示如何检测和使用Python解释器。

我们将介绍find_package命令,这个命令将贯穿本章。

具体实施

我们将逐步建立CMakeLists.txt文件:

  1. 首先,定义CMake最低版本和项目名称。注意,这里不需要任何语言支持:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)project(recipe-01 LANGUAGES NONE)
  1. 然后,使用find_package命令找到Python解释器:
find_package(PythonIterp REQUIRED)
  1. 然后,执行Python命令并捕获它的输出和返回值:
execute_process(	COMMAND	 	${PYTHON_EXECUTABLE} "-c" "print('Hello, world!')"	RESULT_VARIABLE _status	OUTPUT_VARIABLE _hello_world	ERROR_QUIET	OUTPUT_STRIP_TRAILING_WHITESPACE)
  1. 最后,打印Python命令的返回值和输出:
message(STATUS "RESULT_VARIABLE is: ${_status}")message(STATUS "OUTPUT_VARIABLE is: ${_hello_world}")

5.配置项目:

xz@xiaqiu:~/study/cmake/study/example9/build$ cmake ..-- Found PythonInterp: /usr/bin/python (found version "2.7.18") -- RESULT_VARIABLE is: 0-- OUTPUT_VARIABLE is: Hello, world!-- _status="0" ; _hello_world="Hello, world!"-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example9/buildxz@xiaqiu:~/study/cmake/study/example9/build$ 

工作原理

find_package是用于发现和设置包的CMake模块的命令。这些模块包含CMake命令,用于标识系统标准位置中的包。CMake模块文件称为Find.cmake,当调用find_package()时,模块中的命令将会运行。除了在系统上实际查找包模块之外,查找模块还会设置了一些有用的变量,反映实际找到了什么,也可以在自己的CMakeLists.txt中使用这些变量。对于Python解释器,相关模块为FindPythonInterp.cmake附带的设置了一些CMake变量:

  • PYTHONINTERP_FOUND:是否找到解释器
  • PYTHON_EXECUTABLE:Python解释器到可执行文件的路径
  • PYTHON_VERSION_STRING:Python解释器的完整版本信息
  • PYTHON_VERSION_MAJOR:Python解释器的主要版本号
  • PYTHON_VERSION_MINOR :Python解释器的次要版本号
  • PYTHON_VERSION_PATCH:Python解释器的补丁版本号

可以强制CMake,查找特定版本的包。例如,要求Python解释器的版本大于或等于2.7:find_package(PythonInterp 2.7)

可以强制满足依赖关系:

find_package(PythonInterp REQUIRED)

如果在查找位置中没有找到适合Python解释器的可执行文件,CMake将中止配置。

TIPS:CMake有很多查找软件包的模块。我们建议在CMake在线文档中查询*Find.cmake模块,并在使用它们之前详细阅读它们的文档。find_package命令的文档可以参考 https://cmake.org/cmake/help/v3.5/command/find_ackage.html 。在线文档的一个很好的替代方法是浏览 https://github.com/Kitware/CMake/tree/master/Modules 中的CMake模块源代码——它们记录了模块使用的变量,以及模块可以在CMakeLists.txt*中使用的变量。

更多信息

软件包没有安装在标准位置时,CMake无法正确定位它们。用户可以使用CLI的-D参数传递相应的选项,告诉CMake查看特定的位置。Python解释器可以使用以下配置:

$ cmake -D PYTHON_EXECUTABLE=/custom/location/python ..

这将指定非标准/custom/location/pytho安装目录中的Python可执行文件。

NOTE:*每个包都是不同的,**Find.cmake模块试图提供统一的检测接口。当CMake无法找到模块包时,我们建议您阅读相应检测模块的文档,以了解如何正确地使用CMake模块。可以在终端中直接浏览文档,本例中可使用cmake --help-module FindPythonInterp*查看。

除了检测包之外,我们还想提到一个便于打印变量的helper模块。本示例中,我们使用了以下方法:

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!"

检测Python库

NOTE:此示例代码可以在 https://github.com/devcafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-02 中找到,有一个C示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

可以使用Python工具来分析和操作程序的输出。然而,还有更强大的方法可以将解释语言(如Python)与编译语言(如C或C++)组合在一起使用。一种是扩展Python,通过编译成共享库的C或C++模块在这些类型上提供新类型和新功能,这是第9章的主题。另一种是将Python解释器嵌入到C或C++程序中。两种方法都需要下列条件:

  • Python解释器的工作版本
  • Python头文件Python.h的可用性
  • Python运行时库libpython

三个组件所使用的Python版本必须相同。我们已经演示了如何找到Python解释器;本示例中,我们将展示另外两种方式。

具体实施

以下是CMakeLists.txt中的步骤:

  1. 以下是CMakeLists.txt中的步骤:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)project(recipe-02 LANGUAGES C)
  1. 制使用C99标准,这不严格要求与Python链接,但有时你可能需要对Python进行连接:
set(CMAKE_C_STANDARD 99)set(CMAKE_C_EXTENSIONS OFF)set(CMAKE_C_STANDARD_REQUIRED ON)
  1. 找到Python解释器。这是一个REQUIRED依赖:
find_package(PythonInterp 3.8 REQUIRED)
  1. 找到Python头文件和库的模块,称为FindPythonLibs.cmake:
find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
  1. 使用hello-embedded-python.c源文件,添加一个可执行目标:
add_executable(hello-embedded-python hello-embedded-python.c)
  1. 可执行文件包含Python.h头文件。因此,这个目标的include目录必须包含Python的include目录,可以通过PYTHON_INCLUDE_DIRS变量进行指定:
target_include_directories(hello-embedded-python  PRIVATE      ${PYTHON_INCLUDE_DIRS}    )
  1. 最后,将可执行文件链接到Python库,通过PYTHON_LIBRARIES变量访问:
target_link_libraries(hello-embedded-python  PRIVATE      ${PYTHON_LIBRARIES}    )
  1. 现在,进行构建:
xz@xiaqiu:~/study/cmake/study/example10/build$ cmake ..-- The C compiler identification is GNU 9.3.0-- Check for working C compiler: /usr/bin/cc-- Check for working C compiler: /usr/bin/cc -- works-- Detecting C compiler ABI info-- Detecting C compiler ABI info - done-- Detecting C compile features-- Detecting C compile features - done-- Found PythonInterp: /usr/bin/python3.8 (found suitable version "3.8.10", minimum required is "3.8") -- Found PythonLibs: /usr/lib/x86_64-linux-gnu/libpython3.8.so (found suitable exact version "3.8.10") -- PYTHON_INCLUDE_DIRS = /usr/include/python3.8-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example10/buildxz@xiaqiu:~/study/cmake/study/example10/build$ makeScanning dependencies of target hello-embedded-python[ 50%] Building C object CMakeFiles/hello-embedded-python.dir/hello-embedded-python.c.o/home/xz/study/cmake/study/example10/hello-embedded-python.c: In function ‘main’:/home/xz/study/cmake/study/example10/hello-embedded-python.c:5:24: warning: passing argument 1 of ‘Py_SetProgramName’ from incompatible pointer type [-Wincompatible-pointer-types]    5 |  Py_SetProgramName(argv[0]);/* optional but recommended */      |                    ~~~~^~~      |                        |      |                        char *In file included from /usr/include/python3.8/Python.h:140,                 from /home/xz/study/cmake/study/example10/hello-embedded-python.c:1:/usr/include/python3.8/pylifecycle.h:38:36: note: expected ‘const wchar_t *{aka ‘const int *} but argument is of type ‘char *38 | PyAPI_FUNC(void) Py_SetProgramName(const wchar_t *);      |                                    ^~~~~~~~~~~~~~~[100%] Linking C executable hello-embedded-python[100%] Built target hello-embedded-pythonxz@xiaqiu:~/study/cmake/study/example10/build$ ./hello-embedded-python Today is Tue Oct  5 19:28:36 2021xz@xiaqiu:~/study/cmake/study/example10/build$ 

工作原理

FindPythonLibs.cmake模块将查找Python头文件和库的标准位置。由于,我们的项目需要这些依赖项,如果没有找到这些依赖项,将停止配置,并报出错误。注意,我们显式地要求CMake检测安装的Python可执行文件。这是为了确保可执行文件、头文件和库都有一个匹配的版本。这对于不同版本,可能在运行时导致崩溃。我们通过FindPythonInterp.cmake中定义的PYTHON_VERSION_MAJORPYTHON_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头文件和库的位置是正确的?对于Python解释器,可以通过CLI的-D选项传递PYTHON_LIBRARYPYTHON_INCLUDE_DIR选项来强制CMake查找特定的目录。这些选项指定了以下内容:

  • PYTHON_LIBRARY:指向Python库的路径
  • PYTHON_INCLUDE_DIR:Python.h所在的路径

这样,就能获得所需的Python版本。

TIPS:有时需要将*-D PYTHON_EXECUTABLE-D PYTHON_LIBRARY-D PYTHON_INCLUDE_DIR*传递给CMake CLI,以便找到及定位相应的版本的组件。

要将Python解释器及其开发组件匹配为完全相同的版本可能非常困难,对于那些将它们安装在非标准位置或系统上安装了多个版本的情况尤其如此。CMake 3.12版本中增加了新的Python检测模块,旨在解决这个棘手的问题。我们CMakeLists.txt的检测部分也将简化为:

find_package(Python COMPONENTS Interpreter Development REQUIRED)

我们建议您阅读新模块的文档,地址是: https://cmake.org/cmake/help/v3.12/module/FindPython.html

检测Python模块和包

NOTE:此示例代码可以在 https://github.com/devcafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-03 中找到,包含一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

前面的示例中,我们演示了如何检测Python解释器,以及如何编译一个简单的C程序(嵌入Python解释器)。通常,代码将依赖于特定的Python模块,无论是Python工具、嵌入Python的程序,还是扩展Python的库。例如,科学界非常流行使用NumPy处理矩阵问题。依赖于Python模块或包的项目中,确定满足对这些Python模块的依赖非常重要。本示例将展示如何探测用户的环境,以找到特定的Python模块和包。

准备工作

我们将尝试在C++程序中嵌入一个稍微复杂一点的例子。这个示例再次引用Python在线文档,并展示了如何通过调用编译后的C++可执行文件,来执行用户定义的Python模块中的函数。

Python 3示例代码(Py3-pure-embedding.cpp)包含以下源代码(请参见https://docs.python.org/2/extending/embedding.html#pure-embedded 与Python 2代码等效):

/* * Code example from: *    https://docs.python.org/3.5/extending/embedding.html#pure-embedding */#include <Python.h>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 npdef 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)
  1. 查找解释器、头文件和库的方法与前面的方法完全相同:
find_package(PythonInterp REQUIRED)find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
  1. 正确打包的Python模块,指定安装位置和版本。可以在CMakeLists.txt中执行Python脚本进行探测:
execute_process(	COMMAND		${PYTHON_EXECUTABLE} "-c" "import re, numpy; print(re.compile('/__init__.py.*').sub('',numpy.__file__))"	RESULT_VARIABLE _numpy_status	OUTPUT_VARIABLE _numpy_location	ERROR_QUIET	OUTPUT_STRIP_TRAILING_WHITESPACE)
  1. 如果找到NumPy,则_numpy_status变量为整数,否则为错误的字符串,而_numpy_location将包含NumPy模块的路径。如果找到NumPy,则将它的位置保存到一个名为NumPy的新变量中。注意,新变量被缓存,这意味着CMake创建了一个持久性变量,用户稍后可以修改该变量:
if(NOT _numpy_status)	set(numPy ${_numpy_location} CACHE STRING "Location of NumPy")endif()
  1. 下一步是检查模块的版本。同样,我们在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
)
  1. 最后,FindPackageHandleStandardArgs的CMake包以正确的格式设置NumPy_FOUND变量和输出信息:
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(NumPy
  FOUND_VAR NumPy_FOUND
  REQUIRED_VARS NumPy
  VERSION_VAR _numpy_version
  )
  1. 一旦正确的找到所有依赖项,我们就可以编译可执行文件,并将其链接到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}  )
  1. 我们还必须保证use_numpy.pybuild目录中可用:
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 commandtarget_sources(pure-embedding  PRIVATE      ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py  )
  1. 现在,我们可以测试嵌入的代码:
xz@xiaqiu:~/study/cmake/study/example11/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Found PythonInterp: /usr/bin/python3.8 (found suitable version "3.8.10", minimum required is "3.8") -- Found PythonLibs: /usr/lib/x86_64-linux-gnu/libpython3.8.so (found suitable exact version "3.8.10") -- Found NumPy: /home/xz/.local/lib/python3.8/site-packages/numpy (found version "1.21.0") -- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example11/buildxz@xiaqiu:~/study/cmake/study/example11/build$ make[ 33%] Generating use_numpy.pyScanning dependencies of target pure-embedding[ 66%] Building CXX object CMakeFiles/pure-embedding.dir/Py3-pure-embedding.cpp.o[100%] Linking CXX executable pure-embedding[100%] Built target pure-embeddingxz@xiaqiu:~/study/cmake/study/example11/build$ ./pure-embedding use_numpy print_ones 3 3[[1. 1. 1.] [1. 1. 1.] [1. 1. 1.]]Result of call: 9xz@xiaqiu:~/study/cmake/study/example11/build$ ./pure-embedding use_numpy print_ones 3 5[[1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.]]Result of call: 15xz@xiaqiu:~/study/cmake/study/example11/build$ ./pure-embedding use_numpy print_ones 5 5[[1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.]]Result of call: 25xz@xiaqiu:~/study/cmake/study/example11/build$ 

工作原理

例子中有三个新的CMake命令,需要include(FindPackageHandleStandardArgs)

  • execute_process
  • add_custom_command
  • find_package_handle_standard_args

execute_process将作为通过子进程执行一个或多个命令。最后,子进程返回值将保存到变量作为参数,传递给RESULT_VARIABLE,而管道标准输出和标准错误的内容将被保存到变量作为参数传递给OUTPUT_VARIABLEERROR_VARIABLEexecute_process可以执行任何操作,并使用它们的结果来推断系统配置。本例中,用它来确保NumPy可用,然后获得模块版本。

find_package_handle_standard_args提供了,用于处理与查找相关程序和库的标准工具。引用此命令时,可以正确的处理与版本相关的选项(REQUIREDEXACT),而无需更多的CMake代码。稍后将介绍QUIETCOMPONENTS选项。本示例中,使用了以下方法:

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_FOUNDFALSE,则停止配置。最后,将use_numpy.py复制到build目录,对代码进行注释:

add_custom_command(OUTPUT	${CMAKE_CURRENT_BINARY_DIR}/use_numpy.pyCOMMAND	${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/use_numpy.py	${CMAKE_CURRENT_BINARY_DIR}/use_numpy.pyDEPENDS	${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_sources命令,它将依赖项添加到${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py;这样做是为了确保构建目标,能够触发之前的命令。

检测BLAS和LAPACK数学库

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-04 中找到,有一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

许多数据算法严重依赖于矩阵和向量运算。例如:矩阵-向量和矩阵-矩阵乘法,求线性方程组的解,特征值和特征向量的计算或奇异值分解。这些操作在代码库中非常普遍,因为操作的数据量比较大,因此高效的实现有绝对的必要。幸运的是,有专家库可用:基本线性代数子程序(BLAS)和线性代数包(LAPACK),为许多线性代数操作提供了标准API。供应商有不同的实现,但都共享API。虽然,用于数学库底层实现,实际所用的编程语言会随着时间而变化(Fortran、C、Assembly),但是也都是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<double> dist(-1.0, 1.0);

    // Allocate matrices and right-hand side vector
	int dim = std::atoi(argv[1]);
	std::vector<double> A(dim * dim);
	std::vector<double> b(dim);
	std::vector<int> 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<double> A1(A);
	std::vector<double> 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(b[i] - sum);
    }
    std::cout<<"check is "<<eps<<std::endl;

    return  0;
}

使用C++11的随机库来生成-1.0到1.0之间的随机分布。C_DSCALC_DGESV分别是到BLAS和LAPACK库的接口。为了避免名称混淆,将在下面来进一步讨论CMake模块:

文件CxxBLAS.hppextern "C"封装链接BLAS:

#pragma once#include "fc_mangle.h"#include #ifdef __cplusplusextern "C"{#endifextern void DSCAL(int *n, double *alpha, double *vec, int *inc);#ifdef __cplusplus}#endifvoid C_DSCAL(size_t length, double alpha, double *vec, int inc);

对应的实现文件CxxBLAS.cpp:

#include "CxxBLAS.hpp"#include <climits>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.hppCxxLAPACK.cpp为LAPACK调用执行相应的转换。

具体实施

对应的CMakeLists.txt包含以下构建块:

  1. 我们定义了CMake最低版本,项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FALAL_ERROR)project(recipe-04 LANGUANGES cxx c Fortran)
  1. 使用C++11标准:
set(CMAKE_CXX_STANDARD 11)set(CMAKE_CXX_EXTENSIONS OFF)set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 此外,我们验证Fortran和C/C++编译器是否能协同工作,并生成头文件,这个文件可以处理名称混乱。两个功能都由FortranCInterface模块提供:
include(FortranCInterface)FortranCInterface_VERIFY(CXX)FortranCInterface_HEADER(  fc_mangle.h  MACRO_NAMESPACE "FC_"  SYMBOLS DSCAL DGESV  )
  1. 然后,找到BLAS和LAPACK:
find_package(BLAS REQUIRED)find_package(LAPACK REQUIRED)
  1. 接下来,添加一个库,其中包含BLAS和LAPACK包装器的源代码,并链接到LAPACK_LIBRARIES,其中也包含BLAS_LIBRARIES:
add_library(math "")target_source(mathPRIVATECxxBLAS.cppCxxLAPACK.cpp)target_include_directories(math	PUBLIC    ${CMAKE_CURRENT_SOURCE_DIR}    ${CMAKE_CURRENT_BINARY_DIR}  }target_link_libraries(mathPUBLIC${LAPACK_LIBRARIES})
  1. 注意,目标的包含目录和链接库声明为PUBLIC,因此任何依赖于数学库的附加目标也将在其包含目录中。

  2. 最后,我们添加一个可执行目标并链接math

add_executable(linear-algebra "")target_sources(linear-algebraPRIVATElinear-algebra.cpp)target_link_libraries(linear-algebraPRIVATEmath)
  1. 配置时,我们可以关注相关的打印输出:
xz@xiaqiu:~/study/cmake/study/example12/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- The C compiler identification is GNU 9.3.0-- The Fortran compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Check for working C compiler: /usr/bin/cc-- Check for working C compiler: /usr/bin/cc -- works-- Detecting C compiler ABI info-- Detecting C compiler ABI info - done-- Detecting C compile features-- Detecting C compile features - done-- Check for working Fortran compiler: /usr/bin/gfortran-- Check for working Fortran compiler: /usr/bin/gfortran  -- works-- Detecting Fortran compiler ABI info-- Detecting Fortran compiler ABI info - done-- Checking whether /usr/bin/gfortran supports Fortran 90-- Checking whether /usr/bin/gfortran supports Fortran 90 -- yes-- Detecting Fortran/C Interface-- Detecting Fortran/C Interface - Found GLOBAL and MODULE mangling-- Verifying Fortran/CXX Compiler Compatibility-- Verifying Fortran/CXX Compiler Compatibility - Success-- Looking for Fortran sgemm-- Looking for Fortran sgemm - not found-- Looking for pthread.h-- Looking for pthread.h - found-- Performing Test CMAKE_HAVE_LIBC_PTHREAD-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Failed-- Looking for pthread_create in pthreads-- Looking for pthread_create in pthreads - not found-- Looking for pthread_create in pthread-- Looking for pthread_create in pthread - found-- Found Threads: TRUE  -- Looking for Fortran sgemm-- Looking for Fortran sgemm - found-- Found BLAS: /usr/lib/x86_64-linux-gnu/libblas.so  -- Looking for Fortran cheev-- Looking for Fortran cheev - not found-- Looking for Fortran cheev-- Looking for Fortran cheev - found-- A library with LAPACK API found.-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example12/buildxz@xiaqiu:~/study/cmake/study/example12/build$ lsCMakeCache.txt  CMakeFiles  cmake_install.cmake  fc_mangle.h  Makefilexz@xiaqiu:~/study/cmake/study/example12/build$ makeScanning dependencies of target math[ 20%] Building CXX object CMakeFiles/math.dir/CxxBLAS.cpp.o[ 40%] Building CXX object CMakeFiles/math.dir/CxxLAPACK.cpp.o[ 60%] Linking CXX static library libmath.a[ 60%] Built target mathScanning dependencies of target linear-algebra[ 80%] Building CXX object CMakeFiles/linear-algebra.dir/linear-algebra.cpp.o[100%] Linking CXX executable linear-algebra[100%] Built target linear-algebraxz@xiaqiu:~/study/cmake/study/example12/build$ lsCMakeCache.txt  cmake_install.cmake  libmath.a       MakefileCMakeFiles      fc_mangle.h          linear-algebraxz@xiaqiu:~/study/cmake/study/example12/build$ ./linear-algebra Usage: ./linear-algebra dimxz@xiaqiu:~/study/cmake/study/example12/build$ ./linear-algebra 12C_DSCAL doneC_DGESV doneinfo is 0check is 29.8504xz@xiaqiu:~/study/cmake/study/example12/build$ ./linear-algebra 120C_DSCAL doneC_DGESV doneinfo is 0check is 57.302xz@xiaqiu:~/study/cmake/study/example12/build$ 

工作原理

FindBLAS.cmakeFindLAPACK.cmake将在标准位置查找BLAS和LAPACK库。对于前者,该模块有SGEMM函数的Fortran实现,一般用于单精度矩阵乘积。对于后者,该模块有CHEEV函数的Fortran实现,用于计算复杂厄米矩阵的特征值和特征向量。查找在CMake内部,通过编译一个小程序来完成,该程序调用这些函数,并尝试链接到候选库。如果失败,则表示相应库不存于系统上。生成机器码时,每个编译器都会处理符号混淆,不幸的是,这种操作并不通用,而与编译器相关。为了解决这个问题,我们使用FortranCInterface模块( https://cmake.org/cmake/help/v3.5/module/FortranCInterface.html )验证Fortran和C/C++能否混合编译,然后生成一个Fortran-C接口头文件fc_mangle.h,这个文件用来解决编译器性的问题。然后,必须将生成的fc_mann .h包含在接口头文件CxxBLAS.hppCxxLAPACK.hpp中。为了使用FortranCInterface,我们需要在LANGUAGES列表中添加C和Fortran支持。当然,也可以定义自己的预处理器定义,但是可移植性会差很多。我们将在第9章中更详细地讨论Fortran和C的互操作性。

NOTE:目前,BLAS和LAPACK的许多实现已经在Fortran外附带了一层C包装。这些包装器多年来已经标准化,称为CBLAS和LAPACKE。

更多信息

许多算法代码比较依赖于矩阵代数运算,使用BLAS和LAPACK API的高性能实现就非常重要了。供应商为不同的体系结构和并行环境提供不同的库,FindBLAS.cmakeFindLAPACK.cmake可能的无法定位到当前库。如果发生这种情况,可以通过-D选项显式地从CLI对库进行设置。

检测OpenMP的并行环境

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-05 *中找到,有一个C++和一个Fortran示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。*https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-05 中也有一个适用于CMake 3.5的示例。

目前,市面上的计算机几乎都是多核机器,对于性能敏感的程序,我们必须关注这些多核处理器,并在编程模型中使用并发。OpenMP是多核处理器上并行性的标准之一。为了从OpenMP并行化中获得性能收益,通常不需要修改或重写现有程序。一旦确定了代码中的性能关键部分,例如:使用分析工具,程序员就可以通过预处理器指令,指示编译器为这些区域生成可并行的代码。

本示例中,我们将展示如何编译一个包含OpenMP指令的程序(前提是使用一个支持OpenMP的编译器)。有许多支持OpenMP的Fortran、C和C++编译器。对于相对较新的CMake版本,为OpenMP提供了非常好的支持。本示例将展示如何在使用CMake 3.9或更高版本时,使用简单C++和Fortran程序来链接到OpenMP。

NOTE:*根据Linux发行版的不同,Clang编译器的默认版本可能不支持OpenMP。使用或非苹果版本的Clang(例如,Conda提供的)或GNU编译器,除非单独安装libomp库(*https://iscinumpy.gitlab.io/post/omp-on-high-sierra/ ),否则本节示例将无法在macOS上工作。

准备工作

C和C++程序可以通过包含omp.h头文件和链接到正确的库,来使用OpenMP功能。编译器将在性能关键部分之前添加预处理指令,并生成并行代码。在本示例中,我们将构建以下示例源代码(example.cpp)。这段代码从1到N求和,其中N作为命令行参数:

#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;}

具体实施

对于C++和Fortran的例子,CMakeLists.txt将遵循一个模板,该模板在这两种语言上很相似:

  1. 两者都定义了CMake最低版本、项目名称和语言(CXX或Fortran;我们将展示C++版本):
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)project(recipe-05 LANGUAGES CXX)
  1. 使用C++11标准:
set(CMAKE_CXX_STANDARD 11)set(CMAKE_CXX_EXTENSIONS OFF)set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 调用find_package来搜索OpenMP:
find_package(OpenMP REQUIRED)
  1. 最后,我们定义可执行目标,并链接到FindOpenMP模块提供的导入目标(在Fortran的情况下,我们链接到OpenMP::OpenMP_Fortran):
add_executable(example example.cpp)target_link_libraries(examplePUBLIC	OpenMP::OpenMP_CXX)
  1. 现在,可以配置和构建代码了:
xz@xiaqiu:~/study/cmake/study/example13/build$ cmake ..
-- The CXX compiler identification is GNU 9.3.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found OpenMP_CXX: -fopenmp (found version "4.5") 
-- Found OpenMP: TRUE (found version "4.5")  
-- Configuring done
-- Generating done
-- Build files have been written to: /home/xz/study/cmake/study/example13/build
xz@xiaqiu:~/study/cmake/study/example13/build$ make
Scanning dependencies of target example
[ 50%] Building CXX object CMakeFiles/example.dir/example.cpp.o
[100%] Linking CXX executable example
[100%] Built target example
xz@xiaqiu:~/study/cmake/study/example13/build$ ./example  1000
number of available processors: 4
number of threads: 4
we will form sum of numbers from 1 to 1000
sum: 500500
elapsed wall clock time: 0.00554969 seconds
xz@xiaqiu:~/study/cmake/study/example13/build$ 

工作原理

我们的示例很简单:编译代码,并运行在多个内核上时,我们会看到加速效果。加速效果并不是OMP_NUM_THREADS的倍数,不过本示例中并不关心,因为我们更关注的是如何使用CMake配置需要使用OpenMP的项目。我们发现链接到OpenMP非常简单,这要感谢FindOpenMP模块:

target_link_libraries(example    PUBLIC        OpenMP::OpenMP_CXX    )

我们不关心编译标志或包含目录——这些设置和依赖项是在OpenMP::OpenMP_CXX中定义的(IMPORTED类型)。如第1章第3节中提到的,IMPORTED库是伪目标,它完全是我们自己项目的外部依赖项。要使用OpenMP,需要设置一些编译器标志,包括目录和链接库。所有这些都包含在OpenMP::OpenMP_CXX的属性上,并通过使用target_link_libraries命令传递给example。这使得在CMake中,使用库变得非常容易。我们可以使用cmake_print_properties命令打印接口的属性,该命令由CMakePrintHelpers.CMake模块提供:

include(CMakePrintHelpers)cmake_print_properties(    TARGETS        OpenMP::OpenMP_CXX    PROPERTIES        INTERFACE_COMPILE_OPTIONS        INTERFACE_INCLUDE_DIRECTORIES        INTERFACE_LINK_LIBRARIES    )

所有属性都有INTERFACE_前缀,因为这些属性对所需目标,需要以接口形式提供,并且目标以接口的方式使用OpenMP。

所有属性都有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}
  )

检测MPI的并行环境

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-06 *中找到,包含一个C++和一个C的示例。该示例在CMake 3.9版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。*https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-06 中也有一个适用于CMake 3.5的C示例。

消息传递接口(Message Passing Interface, MPI),可以作为OpenMP(共享内存并行方式)的补充,它也是分布式系统上并行程序的实际标准。尽管,最新的MPI实现也允许共享内存并行,但高性能计算中的一种典型方法就是,在计算节点上OpenMP与MPI结合使用。MPI标准的实施包括:

  1. 运行时库
  2. 头文件和Fortran 90模块
  3. 编译器的包装器,用来调用编译器,使用额外的参数来构建MPI库,以处理目录和库。通常,包装器mpic++/mpiCC/mpicxx用于C++,mpicc用于C,mpifort用于Fortran。
  4. 启动MPI:应该启动程序,以编译代码的并行执行。它的名称依赖于实现,可以使用这几个命令启动:mpirunmpiexecorterun

本示例,将展示如何在系统上找到合适的MPI实现,从而编译一个简单的“Hello, World”MPI例程。

准备工作

示例代码(hello-mpi.cpp,可从http://www.mpitutorial.com 下载)将在本示例中进行编译,它将初始化MPI库,让每个进程打印其名称:

add_executable(hello-mpi hello-mpi.c)target_compile_options(hello-mpi                      PUBLIC                       ${MPI_CXX_COMPILE_FLAGS})target_include_directories(hello-mpi                      PUBLIC                       ${MPI_CXX_INCLUDE_PATH})target_link_libraries(hello-mpi                     PUBLIC                     ${MPI_CXX_LIBRARIES}                     )

本示例中,我们讨论了C++项目。其中的参数和方法对于C或Fortran项目同样有效。

检测Eigen库

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-07 *中找到,包含一个C++的示例。该示例在CMake 3.9版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。*https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-06 中也有一个适用于CMake 3.5的C++示例。

BLAS库为矩阵和向量操作提供了标准化接口。不过,这个接口用Fortran语言书写。虽然已经展示了如何使用C++直接使用这些库,但在现代C++程序中,希望有更高级的接口。

纯头文件实现的Eigen库,使用模板编程来提供接口。矩阵和向量的计算,会在编译时进行数据类型检查,以确保兼容所有维度的矩阵。密集和稀疏矩阵的运算,也可使用表达式模板高效的进行实现,如:矩阵-矩阵乘积,线性系统求解器和特征值问题。从3.3版开始,Eigen可以链接到BLAS和LAPACK库中,这可以将某些操作实现进行卸载,使库的实现更加灵活,从而获得更多的性能收益。

本示例将展示如何查找Eigen库,使用OpenMP并行化,并将部分工作转移到BLAS库。

本示例中会实现,矩阵-向量乘法和LU分解,可以选择卸载BLAS和LAPACK库中的一些实现。这个示例中,只考虑将在BLAS库中卸载。

准备工作

本例中,我们编译一个程序,该程序会从命令行获取的随机方阵和维向量。然后我们将用LU分解来解线性方程组Ax=b。以下是源代码(linear-algebra.cpp):

#include 
#include 
#include 
#include 
#include 
#include 

#include 

int main(int argc, char const *argv[])
{
	if(argc != 2)
	{
		std::cout<<"Usage: ./linear-algebra dim"<<std::endl;
		return EXIT_FAILURE;
	}

	std::chrono::time_point<std::chrono::system_clock> start,end;
	std::chrono::duration<double> elapsed_seconds;
	std::time_t end_time;

	std::cout<<"Number of threads used by Eigen: "<<Eigen::nbThreads()
			 <<std::endl;

	// Allocate matrices and right-hand side vector
	start = std::chrono::system_clock::now();
	int dim = std::atoi(argv[1]);
	Eigen::MaxtrixXd A = Eigen::MatrixXd::Random(dim,dim);
	Eigen::VectorXd b = Eigen::VectorXd::Random(dim);
	end = std::chrono::system_clock::now();


	// Report times
	elapsed_seconds = end - start;
	end_time = std::chrono::system_clock::to_time_t(end);
	std::cout<<"matrices allocated and initialized"
			 <<std::put_time(std::localtime(&end_time),"%a %b %d %Y %r\n")
			 <<"elapse time: "<<elapsed_seconds.count()<<"s\n";
	start = std::chrono::system_clock::now();
	//Save matrix and RHS
	Eigen::MatrixXd A1 = A;
	Eigen::VectorXd b1 = b;
	end = std::chrono::system_clock::now();
	end_time = std::chrono::system_clock::to_time_t(endl);	
	std::cout<<"Scaling done,A and b saved"
			 <<std::put_time(std::localtime(&end_time),"%a %b %d %Y %r\n")
			 <<"elapsed time: "<<elapsed_seconds.count()<<"s\n";

	start =  std::chrono::system_clock::now();
	Eigen::VectorXd x = A.lu().solve(b);
	end = std::chrono::system_clock::now();

	//Report times
	elapsed_seconds = end - start;
	end_time = std::chrono::system_clock::to_time_t(end);

	double relative_error = (A * x - b).norm() / b.norm();

	std::cout<<"Linear system solver done "
			 <<std::put_time(std::localtime(&end_time),"%a %b %d %Y %r\n")
			 <<"elapsed time: "<<elapsed_seconds.count()<<"s\n";
	std::cout<<"relative error is "<<relative_error <<std::endl;

	return 0;
}

矩阵-向量乘法和LU分解是在Eigen库中实现的,但是可以选择BLAS和LAPACK库中的实现。在这个示例中,我们只考虑BLAS库中的实现。

具体实施

这个示例中,我们将用到Eigen和BLAS库,以及OpenMP。使用OpenMP将Eigen并行化,并从BLAS库中卸载部分线性代数实现:

  1. 首先声明CMake最低版本、项目名称和使用C++11语言标准:
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)project(recipe-07 LANGUAGES CXX)set(CMAKE_CXX_STANDARD 11)set(CMAKE_CXX_EXTENSIONS OFF)set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 因为Eigen可以使用共享内存的方式,所以可以使用OpenMP并行处理计算密集型操作:
find_package(OpenMP REQUIRED)
  1. 调用find_package来搜索Eigen(将在下一小节中讨论):
find_package(Eigen3 3.3 REQUIRED CONFIG)
  1. 如果找到Eigen,我们将打印状态信息。注意,使用的是Eigen3::Eigen,这是一个IMPORT目标,可通过提供的CMake脚本找到这个目标:
if(TARGET Eigen3::Eigen)  message(STATUS "Eigen3 v${EIGEN3_VERSION_STRING} found in ${EIGEN3_INCLUDE_DIR}")endif()
  1. 接下来,将源文件声明为可执行目标:
add_executable(linear-algebra linear-algebra.cpp)
  1. 然后,找到BLAS。注意,现在不需要依赖项:
find_package(BLAS)
  1. 如果找到BLAS,我们可为可执行目标,设置相应的宏定义和链接库:
if(BLAS_FOUND)  message(STATUS "Eigen will use some subroutines from BLAS.")  message(STATUS "See: http://eigen.tuxfamily.org/dox-devel/TopicUsingBlasLapack.html")  target_compile_definitions(linear-algebra    PRIVATE        EIGEN_USE_BLAS    )  target_link_libraries(linear-algebra    PUBLIC        ${BLAS_LIBRARIES}    )else()    message(STATUS "BLAS not found. Using Eigen own functions")endif()
  1. 最后,我们链接到Eigen3::EigenOpenMP::OpenMP_CXX目标。这就可以设置所有必要的编译标示和链接标志:
target_link_libraries(linear-algebra  PUBLIC    Eigen3::Eigen    OpenMP::OpenMP_CXX  )
  1. 开始配置:
xz@xiaqiu:~/study/cmake/cmake-cookbook/chapter-03/recipe-07/cxx-example/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Found OpenMP_CXX: -fopenmp (found version "4.5") -- Found OpenMP: TRUE (found version "4.5")  -- Eigen3 v3.3.7 found in /usr/include/eigen3-- Looking for sgemm_-- Looking for sgemm_ - not found-- Looking for C++ include pthread.h-- Looking for C++ include pthread.h - found-- Performing Test CMAKE_HAVE_LIBC_PTHREAD-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Failed-- Looking for pthread_create in pthreads-- Looking for pthread_create in pthreads - not found-- Looking for pthread_create in pthread-- Looking for pthread_create in pthread - found-- Found Threads: TRUE  -- Looking for sgemm_-- Looking for sgemm_ - found-- Found BLAS: /usr/lib/x86_64-linux-gnu/libblas.so  -- Eigen will use some subroutines from BLAS.-- See: http://eigen.tuxfamily.org/dox-devel/TopicUsingBlasLapack.html-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/cmake-cookbook/chapter-03/recipe-07/cxx-example/buildxz@xiaqiu:~/study/cmake/cmake-cookbook/chapter-03/recipe-07/cxx-example/build$ makeScanning dependencies of target linear-algebra[ 50%] Building CXX object CMakeFiles/linear-algebra.dir/linear-algebra.cpp.o[100%] Linking CXX executable linear-algebra[100%] Built target linear-algebraxz@xiaqiu:~/study/cmake/cmake-cookbook/chapter-03/recipe-07/cxx-example/build$ ./linear-algebra Usage: ./linear-algebra dimxz@xiaqiu:~/study/cmake/cmake-cookbook/chapter-03/recipe-07/cxx-example/build$ ./linear-algebra 1000Number of threads used by Eigen: 4matrices allocated and initialized Wed Oct 06 2021 03:09:49 PMelapsed time: 0.055516sScaling done, A and b saved Wed Oct 06 2021 03:09:49 PMelapsed time: 0.055516sLinear system solver done Wed Oct 06 2021 03:09:50 PMelapsed time: 0.62024srelative error is 4.21946e-13xz@xiaqiu:~/study/cmake/cmake-cookbook/chapter-03/recipe-07/cxx-example/build$ 

工作原理

Eigen支持CMake查找,这样配置项目就会变得很容易。从3.3版开始,Eigen提供了CMake模块,这些模块将导出相应的目标Eigen3::Eigen

find_package可以通过选项传递,届时CMake将不会使用FindEigen3.cmake模块,而是通过特定的Eigen3Config.cmakeEigen3ConfigVersion.cmakeEigen3Targets.cmake提供Eigen3安装的标准位置(/share/eigen3/cmake)。这种包定位模式称为“Config”模式,比Find.cmake方式更加通用。有关“模块”模式和“配置”模式的更多信息,可参考官方文档 https://cmake.org/cmake/help/v3.5/command/find_package.html 。

虽然Eigen3、BLAS和OpenMP声明为PUBLIC依赖项,但EIGEN_USE_BLAS编译定义声明为PRIVATE。可以在单独的库目标中汇集库依赖项,而不是直接链接可执行文件。使用PUBLIC/PRIVATE关键字,可以根据库目标的依赖关系调整相应标志和定义。

更多信息

CMake将在预定义的位置层次结构中查找配置模块。首先是CMAKE_PREFIX_PATH_DIR是接下来的搜索路径。因此,如果Eigen3安装在非标准位置,可以使用这两个选项来告诉CMake在哪里查找它:

  1. 通过将Eigen3的安装前缀传递给CMAKE_PREFIX_PATH:
$ cmake -D CMAKE_PREFIX_PATH=<installation-prefix> ..
  1. 通过传递配置文件的位置作为Eigen3_DIR:
$ cmake -D Eigen3_DIR=<installation-prefix>/share/eigen3/cmake ..

检测Boost库

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-08 中找到,包含一个C++的示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

Boost是一组C++通用库。这些库提供了许多功能,这些功能在现代C++项目中不可或缺,但是还不能通过C++标准使用这些功能。例如,Boost为元编程、处理可选参数和文件系统操作等提供了相应的组件。这些库中有许多特性后来被C++11、C++14和C++17标准所采用,但是对于保持与旧编译器兼容性的代码库来说,许多Boost组件仍然是首选。

准备工作

我们将编译的源码是Boost提供的文件系统库与文件系统交互的示例。这个库可以跨平台使用,并将操作系统和文件系统之间的差异抽象为一致的API。下面的代码(path-info.cpp)将接受一个路径作为参数,并将其组件的报告打印到屏幕上:

#include 

#include 

using namespace std;
using namespace boost::filesystem;

const char *say_what(bool b) { return b ? "true" : "false";}

int main(int argc, char *argv[])
{
    if (argc < 2)
    {
        cout
                << "Usage: path_info path-element [path-element...]\n"
                "Composes a path via operator/= from one or more path-element arguments\n"
                "Example: path_info foo/bar baz\n"
#ifdef BOOST_POSIX_API
                "         would report info about the composed path foo/bar/baz\n";
#else // BOOST_WINDOWS_API
                "         would report info about the composed path foo/bar\\baz\n";
#endif
        return 1;
    }
    path p;
    for (; argc > 1; --argc, ++argv)
        p /= argv[1]; // compose path p from the command line arguments

    cout << "\ncomposed path:\n";
    cout << " operator<<()---------: " << p << "\n";
    cout << " make_preferred()-----: " << p.make_preferred() << "\n";
    cout << "\nelements:\n";
    for (auto element : p)
        cout << " " << element << '\n';
    cout << "\nobservers, native format:" << endl;
#ifdef BOOST_POSIX_API
    cout << " native()-------------: " << p.native() << endl;
    cout << "  c_str()--------------: " << p.c_str() << endl;
#else //BOOST_WINDOWS_API
    wcout << L"  native()-------------: " << p.native() << endl;
    wcout << L"	 c_str()--------------: " << p.c_str() << endl;
#endif
    cout << "  string()-- -----------: " << p.string() << endl;
    wcout << L"  wstring()------------: " << p.wstring() << endl;

    cout << "\nobservers, generic format:\n";
    cout << "  generic_string()-----: " << p.generic_string() << endl;
    wcout << L"  generic_wstring()----: " << p.generic_wstring() << endl;

    cout << "\ndecomposition:\n";
    cout << "  root_name()----------: " << p.root_name() << '\n';
    cout << "  root_directory()-----: " << p.root_directory() << '\n';
    cout << "  root_path()----------: " << p.root_path() << '\n';
    cout << "  relative_path()------: " << p.relative_path() << '\n';
    cout << "  parent_path()--------: " << p.parent_path() << '\n';
    cout << "  filename()-----------: " << p.filename() << '\n';
    cout << "  stem()---------------: " << p.stem() << '\n';
    cout << "  extension()----------: " << p.extension() << '\n';

    cout << "\nquery:\n";
    cout << "  empty()--------------: " << say_what(p.empty()) << '\n';
    cout << "  is_absolute()--------: " << say_what(p.is_absolute()) << '\n';
    cout << "  has_root_name()------: " << say_what(p.has_root_name()) << '\n';
    cout << "  has_root_directory()-: " << say_what(p.has_root_directory()) << '\n';
    cout << "  has_root_path()------: " << say_what(p.has_root_path()) << '\n';
    cout << "  has_relative_path()--: " << say_what(p.has_relative_path()) << '\n';
    cout << "  has_parent_path()----: " << say_what(p.has_parent_path()) << '\n';
    cout << "  has_filename()-------: " << say_what(p.has_filename()) << '\n';
    cout << "  has_stem()-----------: " << say_what(p.has_stem()) << '\n';
    cout << "  has_extension()------: " << say_what(p.has_extension()) << '\n';
    return 0;
}

具体实施

Boost由许多不同的库组成,这些库可以独立使用。CMake可将这个库集合,表示为组件的集合。FindBoost.cmake模块不仅可以搜索库集合的完整安装,还可以搜索集合中的特定组件及其依赖项(如果有的话)。我们将逐步建立相应的CMakeLists.txt:

  1. 首先,声明CMake最低版本、项目名称、语言,并使用C++11标准:
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)
  1. 然后,使用find_package搜索Boost。若需要对Boost强制性依赖,需要一个参数。这个例子中,只需要文件系统组件,所以将它作为参数传递给find_package:
find_package(Boost 1.54 REQUIRED COMPONENTS filesystem)
  1. 添加可执行目标,编译源文件:
add_executable(path-info path-info.cpp)
  1. 最后,将目标链接到Boost库组件。由于依赖项声明为PUBLIC,依赖于Boost的目标将自动获取依赖项:
target_link_libraries(path-info  PUBLIC      Boost::filesystem    )

工作原理

FindBoost.cmake是本示例中所使用的CMake模块,其会在标准系统安装目录中找到Boost库。由于我们链接的是Boost::filesystem,CMake将自动设置包含目录并调整编译和链接标志。如果Boost库安装在非标准位置,可以在配置时使用BOOST_ROOT变量传递Boost安装的根目录,以便让CMake搜索非标准路径:

$ cmake -D BOOST_ROOT=/custom/boost

或者,可以同时传递包含头文件的BOOST_INCLUDEDIR变量和库目录的BOOST_LIBRARYDIR变量:

$ cmake -D BOOST_INCLUDEDIR=/custom/boost/include -DBOOST_LIBRARYDIR=/custom/boost/lib

检测外部库:Ⅰ. 使用pkg-config

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-09 *中找到,包含一个C的示例。该示例在CMake 3.6版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。*https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-09 中也有一个适用于CMake 3.5的示例。

目前为止,我们已经讨论了两种检测外部依赖关系的方法:

  • 使用CMake自带的find-module,但并不是所有的包在CMake的find模块都找得到。
  • 使用Config.cmake, ConfigVersion.cmakeTargets.cmake,这些文件由软件包供应商提供,并与软件包一起安装在标准位置的cmake文件夹下。
  • 如果某个依赖项既不提供查找模块,也不提供供应商打包的CMake文件,该怎么办?在这种情况下,我们只有两个选择:
    • 依赖pkg-config程序,来找到系统上的包。这依赖于包供应商在.pc配置文件中,其中有关于发行包的元数据。
    • 为依赖项编写自己的find-package模块。

本示例中,将展示如何利用CMake中的pkg-config来定位ZeroMQ消息库。下一个示例中,将编写一个find模块,展示如何为ZeroMQ编写属于自己find模块。

准备工作

我们构建的代码来自ZeroMQ手册 http://zguide.zeromq.org/page:all 的示例。由两个源文件hwserver.chwclient.c组成,这两个源文件将构建为两个独立的可执行文件。执行时,它们将打印“Hello, World”。

具体实施

这是一个C项目,我们将使用C99标准,逐步构建CMakeLists.txt文件:

  1. 声明一个C项目,并要求符合C99标准:
cmake_minimum_required(VERSION 3.6 FATAL_ERROR)project(recipe-09 LANGUAGES C)set(CMAKE_C_STANDARD 99)set(CMAKE_C_EXTENSIONS OFF)set(CMAKE_C_STANDARD_REQUIRED ON)
  1. 使用CMake附带的find-module,查找pkg-config。这里在find_package中传递了QUIET参数。只有在没有找到pkg-config时,CMake才会报错:
find_package(PkgConfig REQUIRED QUIET)
  1. 找到pkg-config时,我们将使用pkg_search_module函数,以搜索任何附带包配置.pc文件的库或程序。该示例中,我们查找ZeroMQ库:
pkg_search_module( ZeroMQ  REQUIRED      libzeromq libzmq lib0mq  IMPORTED_TARGET)
  1. 如果找到ZeroMQ库,则打印状态消息:
if(TARGET PkgConfig::ZeroMQ)    message(STATUS "Found ZeroMQ")endif()
  1. 然后,添加两个可执行目标,并链接到ZeroMQ。这将自动设置包括目录和链接库:
add_executable(hwserver hwserver.c)target_link_libraries(hwserver PkgConfig::ZeroMQ)add_executable(hwclient hwclient.c)target_link_libraries(hwclient PkgConfig::ZeroMQ)
  1. 现在,我们可以配置和构建示例:
xz@xiaqiu:~/study/cmake/study/example16/build$ cmake ..-- The CXX compiler identification is GNU 9.3.0-- The C compiler identification is GNU 9.3.0-- Check for working CXX compiler: /usr/bin/c++-- Check for working CXX compiler: /usr/bin/c++ -- works-- Detecting CXX compiler ABI info-- Detecting CXX compiler ABI info - done-- Detecting CXX compile features-- Detecting CXX compile features - done-- Check for working C compiler: /usr/bin/cc-- Check for working C compiler: /usr/bin/cc -- works-- Detecting C compiler ABI info-- Detecting C compiler ABI info - done-- Detecting C compile features-- Detecting C compile features - done-- Checking for one of the modules 'libzeromq;libzmq;lib0mq'-- Found ZeroMQ-- Configuring done-- Generating done-- Build files have been written to: /home/xz/study/cmake/study/example16/buildxz@xiaqiu:~/study/cmake/study/example16/build$ makeScanning dependencies of target hwclient[ 25%] Building C object CMakeFiles/hwclient.dir/hwclient.c.o[ 50%] Linking C executable hwclient[ 50%] Built target hwclientScanning dependencies of target hwserver[ 75%] Building C object CMakeFiles/hwserver.dir/hwserver.c.o[100%] Linking C executable hwserver[100%] Built target hwserverxz@xiaqiu:~/study/cmake/study/example16/build$ 

检测外部库:Ⅱ. 自定义find模块

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-10 中找到,包含一个C的示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

此示例补充了上一节的示例,我们将展示如何编写一个find模块来定位系统上的ZeroMQ消息库,以便能够在非Unix操作系统上检测该库。我们重用服务器-客户端示例代码。

如何实施

这是一个C项目,使用C99标准,并逐步构建CMakeLists.txt文件:

  1. 声明一个C项目,并要求符合C99标准:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

project(recipe-10 LANGUAGES C)

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_C_STANDARD_REQUIRED ON)
  1. 将当前源目录CMAKE_CURRENT_SOURCE_DIR,添加到CMake将查找模块的路径列表CMAKE_MODULE_PATH中。这样CMake就可以找到,我们自定义的FindZeroMQ.cmake模块:
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
  1. 现在FindZeroMQ.cmake模块是可用的,可以通过这个模块来搜索项目所需的依赖项。由于我们没有使用QUIET选项来查找find_package,所以当找到库时,状态消息将自动打印:
find_package(ZeroMQ REQUIRED)
  1. 我们继续添加hwserver可执行目标。头文件包含目录和链接库是使用find_package命令成功后,使用ZeroMQ_INCLUDE_DIRSZeroMQ_LIBRARIES变量进行指定的:
add_executable(hwserver hwserver.c)target_include_directories(hwserver  PRIVATE      ${ZeroMQ_INCLUDE_DIRS}  )target_link_libraries(hwserver  PRIVATE      ${ZeroMQ_LIBRARIES}  )
  1. 最后,我们对hwclient可执行目标执行相同的操作:
add_executable(hwclient hwclient.c)target_include_directories(hwclient  PRIVATE      ${ZeroMQ_INCLUDE_DIRS}  )target_link_libraries(hwclient  PRIVATE      ${ZeroMQ_LIBRARIES}  )

此示例的主CMakeLists.txt在使用FindZeroMQ.cmake时,与前一个示例中使用的CMakeLists.txt不同。这个模块使用find_pathfind_library CMake内置命令,搜索ZeroMQ头文件和库,并使用find_package_handle_standard_args设置相关变量,就像我们在第3节中做的那样。

  1. FindZeroMQ.cmake中,检查了ZeroMQ_ROOT变量是否设置。此变量可用于ZeroMQ库的检测,并引导到自定义安装目录。用户可能设置了ZeroMQ_ROOT作为环境变量,我们也会进行检查了:
if(NOT ZeroMQ_ROOT)	set(ZeroMQ_ROOT "$ENV{ZeroMQ_ROOT}")endif()
  1. 然后,搜索系统上zmq.h头文件的位置。这是基于_ZeroMQ_ROOT变量和find_path命令进行的:
if(NOT ZeroMQ_ROOT)	find_path(_ZeroMQ_ROOT NAMES include/zmq.h)else()	set(_ZeroMQ_ROOT "${ZeroMQ_ROOT}")endif()find_path(ZeroMQ_INCLUDE_DIRS NAMES zmq.h HINTS ${_ZeroMQ_ROOT}/include)
  1. 如果成功找到头文件,则将ZeroMQ_INCLUDE_DIRS设置为其位置。我们继续通过使用字符串操作和正则表达式,寻找相应版本的ZeroMQ库:
set(_ZeroMQ_H ${ZeroMQ_INCLUDE_DIRS}/zmq.h)
function(_zmqver_EXTRACT _ZeroMQ_VER_COMPONENT _ZeroMQ_VER_OUTPUT)
set(CMAKE_MATCH_1 "0")
set(_ZeroMQ_expr "^[ \\t]*#define[ \\t]+${_ZeroMQ_VER_COMPONENT}[ \\t]+([0-9]+)$")
file(STRINGS "${_ZeroMQ_H}" _ZeroMQ_ver REGEX "${_ZeroMQ_expr}")
string(REGEX MATCH "${_ZeroMQ_expr}" ZeroMQ_ver "${_ZeroMQ_ver}")
set(${_ZeroMQ_VER_OUTPUT} "${CMAKE_MATCH_1}" PARENT_SCOPE)
endfunction()

_zmqver_EXTRACT("ZMQ_VERSION_MAJOR" ZeroMQ_VERSION_MAJOR)
_zmqver_EXTRACT("ZMQ_VERSION_MINOR" ZeroMQ_VERSION_MINOR)
_zmqver_EXTRACT("ZMQ_VERSION_PATCH" ZeroMQ_VERSION_PATCH)
  1. 然后,为find_package_handle_standard_args准备ZeroMQ_VERSION变量:
if(ZeroMQ_FIND_VERSION_COUNT_CREATE 2)
	set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}")
else()
	set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}")
endif()
  1. 使用find_library命令搜索ZeroMQ库。因为库的命名有所不同,这里我们需要区分Unix的平台和Windows平台:
#Variables
#ZeroMQ_ROOT - set this to a location where ZeroMQ may be found

#ZeroMQ_ROOT - True of ZeroMQ found
#ZeroMQ_INCLUDE_DIRS - Location of ZeroMQ includes
#ZeroMQ_LIBRARIES - ZeroMQ libraries

if(NOT ZeroMQ_ROOT)
	set(ZeroMQ_ROOT "$ENV{ZeroMQ_ROOT}")
endif()

if(NOT ZeroMQ_ROOT)
  find_path(_ZeroMQ_ROOT NAMES include/zmq.h)
else()
  set(_ZeroMQ_ROOT "${ZeroMQ_ROOT}")
endif()

find_path(ZeroMQ_INCLUDE_DIRS NAMES zmq.h HINTS ${_ZeroMQ_ROOT}/include)

if(ZeroMQ_INCLUDE_DIRS)
  set(_ZeroMQ_H ${ZeroMQ_INCLUDE_DIRS}/zmq.h)

  function(_zmqver_EXTRACT _ZeroMQ_VER_COMPONENT _ZeroMQ_VER_OUTPUT)
    set(CMAKE_MATCH_1 "0")
    set(_ZeroMQ_expr "^[ \\t]*#define[ \\t]+${_ZeroMQ_VER_COMPONENT}[ \\t]+([0-9]+)$")
    file(STRINGS "${_ZeroMQ_H}" _ZeroMQ_ver REGEX "${_ZeroMQ_expr}")
    string(REGEX MATCH "${_ZeroMQ_expr}" ZeroMQ_ver "${_ZeroMQ_ver}")
    set(${_ZeroMQ_VER_OUTPUT} "${CMAKE_MATCH_1}" PARENT_SCOPE)
  endfunction()

  _zmqver_EXTRACT("ZMQ_VERSION_MAJOR" ZeroMQ_VERSION_MAJOR)
  _zmqver_EXTRACT("ZMQ_VERSION_MINOR" ZeroMQ_VERSION_MINOR)
  _zmqver_EXTRACT("ZMQ_VERSION_PATCH" ZeroMQ_VERSION_PATCH)

  # We should provide version to find_package_handle_standard_args in the same format as it was requested,
  # otherwise it can't check whether version matches exactly.
  if(ZeroMQ_FIND_VERSION_COUNT GREATER 2)
    set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}.${ZeroMQ_VERSION_PATCH}")
  else()
    # User has requested ZeroMQ version without patch part => user is not interested in specific patch =>
    # any patch should be an exact match.
    set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}")
  endif()

  if(NOT ${CMAKE_C_PLATFORM_ID} STREQUAL "Windows")
    find_library(ZeroMQ_LIBRARIES 
        NAMES 
          zmq 
        HINTS 
          ${_ZeroMQ_ROOT}/lib
          ${_ZeroMQ_ROOT}/lib/x86_64-linux-gnu
        )
  else()
    find_library(ZeroMQ_LIBRARIES
        NAMES
          libzmq
          "libzmq-mt-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
          "libzmq-${CMAKE_VS_PLATFORM_TOOLSET}-mt-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
          libzmq_d
          "libzmq-mt-gd-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
          "libzmq-${CMAKE_VS_PLATFORM_TOOLSET}-mt-gd-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
        HINTS
          ${_ZeroMQ_ROOT}/lib
        )
  endif()
endif()

include(FindPackageHandleStandardArgs)

find_package_handle_standard_args(ZeroMQ
  FOUND_VAR
    ZeroMQ_FOUND
  REQUIRED_VARS
    ZeroMQ_INCLUDE_DIRS
    ZeroMQ_LIBRARIES
  VERSION_VAR
    ZeroMQ_VERSION
  )

工作原理

find-module通常遵循特定的模式:

  1. 检查用户是否为所需的包提供了自定义位置。
  2. 使用find_家族中的命令搜索所需包的必需组件,即头文件、库、可执行程序等等。我们使用find_path查找头文件的完整路径,并使用find_library查找库。CMake还提供find_filefind_programfind_package。这些命令的签名如下:
find_path( NAMES name PATHS paths)
  1. 如果搜索成功,将保存搜索结果;如果搜索失败,则会设置为-NOTFOUNDNAMESPATHS分别是CMake应该查找的文件的名称和搜索应该指向的路径。
  2. 初步搜索的结果中,可以提取版本号。示例中,ZeroMQ头文件包含库版本,可以使用字符串操作和正则表达式提取库版本信息。
  3. 最后,调用find_package_handle_standard_args命令。处理find_package命令的REQUIREDQUIET和版本参数,并设置ZeroMQ_FOUND变量。

NOTE:*任何CMake命令的完整文档都可以从命令行获得。例如,**cmake --help-command find_file将输出find_file命令的手册页。对于CMake标准模块的手册,可以在CLI使用--help-module看到。例如,cmake --help-module FindPackageHandleStandardArgs将输出FindPackageHandleStandardArgs.cmake*的手册页面。

更多信息

总而言之,有四种方式可用于找到依赖包:

  1. 使用由包供应商提供CMake文件Config.cmakeConfigVersion.cmakeTargets.cmake,通常会在包的标准安装位置查找。
  2. 无论是由CMake还是第三方提供的模块,为所需包使用find-module
  3. 使用pkg-config,如本节的示例所示。
  4. 如果这些都不可行,那么编写自己的find模块。

这四种可选方案按相关性进行了排序,每种方法也都有其挑战。

目前,并不是所有的包供应商都提供CMake的Find文件,不过正变得越来越普遍。因为导出CMake目标,使得第三方代码很容易使用它所依赖的库和/或程序附加的依赖。

从一开始,Find-module就一直是CMake中定位依赖的主流手段。但是,它们中的大多数仍然依赖于设置依赖项使用的变量,比如Boost_INCLUDE_DIRSPYTHON_INTERPRETER等等。这种方式很难在第三方发布自己的包时,确保依赖关系被满足。

使用pkg-config的方法可以很好地进行适配,因为它已经成为Unix系统的标准。然而,也由于这个原因,它不是一个完全跨平台的方法。此外,如CMake文档所述,在某些情况下,用户可能会意外地覆盖检测包,并导致pkg-config提供不正确的信息。最后的方法是编写自己的查找模块脚本,就像本示例中那样。这是可行的,并且依赖于FindPackageHandleStandardArgs.cmake。然而,编写一个全面的查找模块脚本绝非易事;有需要考虑很多可能性,我们在Unix和Windows平台上,为查找ZeroMQ库文件演示了一个例子。

你可能感兴趣的:(cmake,git,windows)