CMake入门教程

1. 什么是 CMake?

从 CMake 的网站:https://cmake.org/

CMake 是一个可扩展的开源系统,它在操作系统中以独立于编译器的方式管理构建过程。与许多跨平台系统不同,CMake 旨在与本机构建环境结合使用。放置在每个源目录中的简单配置文件(称为 CMakeLists.txt 文件)用于生成以通常方式使用的标准构建文件(例如,Unix 上的 makefile 和 Windows MSVC 中的项目/工作区)。CMake 可以生成本机构建环境,该环境将编译源代码、创建库、生成包装器并以任意组合构建可执行文件。CMake 支持就地和异地构建,因此可以支持来自单个源树的多个构建。CMake 还支持静态和动态库构建。CMake 的另一个不错的功能是它会生成一个缓存文件,该文件旨在与图形编辑器一起使用。例如,当 CMake 运行时,它会定位包含文件、库和可执行文件,并且可能会遇到可选的构建指令。此信息被收集到缓存中,用户可以在生成本机构建文件之前对其进行更改。

简而言之,CMake 可帮助您有效地管理和构建源代码。如果你在使用 gcc 和 Makefile 时遇到了一些问题,那就移出 CMake 吧。

2.安装

CMake 官方下载网址: https://cmake.org/download/

这里以 win10 64 位系统为例,点击下载 cmake-3.24.0-win64-x64.ms i

要在 Linux 中安装 CMake,只需在终端上执行

# For Ubuntu

$ sudo apt-get install cmake

# For Redhat

$ yum install cmake

# For Mac OS X with Macports

$ sudo port install cmake

3. 快速上手

所以我假设你知道 C++ 和 Makefile 是什么。从现在开始,CMake 将完成 Makefile 的工作。让我们从一个简单的 C++ 程序开始。

// test.cpp

#include <iostream>

using namespace std;

int main(void) {

     cout << "Hello World" << endl;

     return(0);

}

你把它保存为test.cpp,然后要在 CMake 中编译它,你应该创建一个名为的 txt 文件CMakeLists.txt

# Specify the minimum version for CMake

cmake_minimum_required(VERSION 2.8)

# Project's name

project(hello)
# Set the output folder where your program will be created
set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/bin)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR})

# The following folder will be included
include_directories("${PROJECT_SOURCE_DIR}")

有一个 CMake 的全局变量列表。你应该认识他们。

CMAKE_BINARY_DIR

如果您在源代码中构建,则与 相同CMAKE_SOURCE_DIR,否则这是构建树的顶级目录

CMAKE_SOURCE_DIR

这是启动 cmake 的目录,即顶级源目录

EXECUTABLE_OUTPUT_PATH

设置此变量以指定 CMake 应放置所有可执行文件的公共位置(而不是CMAKE_CURRENT_BINARY_DIR

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

LIBRARY_OUTPUT_PATH

设置此变量以指定 CMake 应放置所有库的公共位置(而不是CMAKE_CURRENT_BINARY_DIR

SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

项目名称

PROJECT()命令设置的项目名称。

PROJECT_SOURCE_DIR

包含项目源目录根目录的完整路径,即CMakeLists.txt包含 PROJECT() 命令的最近目录现在,您必须编译test.cpp. 完成这项任务的方法太简单了。将以下行添加到您的CMakeLists.txt:

add_executable(hello ${PROJECT_SOURCE_DIR}/test.cpp)

现在,让我们使用 CMake 构建源代码。此时,您将拥有包含以下文件的文件夹:

$ ls
test.cpp   CMakeLists.txt

要构建您的项目hello,只需执行

$ cmake -H. -Bbuild
$ cmake --build build -- -j3

第一个命令将在文件夹中创建 CMake 配置文件build,第二个命令将在文件夹中生成输出程序 hello bin。你应该测试你的输出:

$ ./bin/hello
Hello World

太简单了吧?

CMake 详细教程

我阅读了大量文档和教程来了解 CMake 及其最常用的实现和设计可靠构建过程的方法。我希望这篇文章可以帮助其他正在学习 CMake 的人。

介绍

CMake 是一个可扩展的开源系统,它在操作系统中以独立于编译器的方式管理构建过程。

与许多跨平台系统不同,CMake 旨在与本机构建环境结合使用。放置在每个源目录中的简单配置文件(称为CMakeLists.txt文件)用于生成以通常方式使用的标准构建文件(例如,Unix 上的 Makefiles 和 Windows MSVC 中的项目/工作区)。

CMake 可以生成本机构建环境,该环境将编译源代码、创建库、生成包装器并以任意组合构建可执行二进制文件。CMake 支持就地和异地构建,因此可以支持来自单个源树的多个构建。

CMake 支持静态和动态库构建。CMake 的另一个不错的功能是它可以生成一个缓存文件,该文件旨在与图形编辑器一起使用。例如,当 CMake 运行时,它会定位包含文件、库和可执行文件,并且可能会遇到可选的构建指令。此信息被收集到缓存中,用户可以在生成本机构建文件之前对其进行更改。

CMake 脚本还使源代码管理更容易,因为它将构建脚本简化为一个文件和更有条理、更易读的格式。

使用 CMake 的流行开源项目

以下是使用 CMake 进行构建的流行开源项目列表:

  • OpenCV:https ://github.com/opencv/opencv
  • Caffe2:https ://github.com/caffe2/caffe2
  • MySql 服务器:https ://github.com/mysql/mysql-server

有关更长的列表,请参见 Wikipedia https://en.wikipedia.org/wiki/CMake#Applications_that_use_CMake

阅读广泛用于了解最佳实践的开源项目始终是一种好习惯。

CMake 作为脚本语言

CMake 旨在成为一个跨平台的构建过程管理器,因此它将它定义为具有某些语法和内置功能的自己的脚本语言。CMake 本身是一个软件程序,因此应使用脚本文件调用它来解释和生成实际的构建文件。

开发人员可以使用 CMake 语言为项目编写简单或复杂的构建脚本。

使用 CMake 语言的构建逻辑和定义要么写在文件中,要么CMakeLists.txt以文件结尾,<project_name>.cmake.但作为最佳实践,主脚本被命名为CMakeLists.txt 而不是 cmake。

  • CMakeLists.txt文件放置在您要构建的项目的源代码中。
  • CMakeLists.txt被放置在任何应用程序的源代码树的根目录下,它可以工作的库。
  • 如果有多个模块,并且每个模块可以单独编译和构建,CMakeLists.txt可以插入到子文件夹中。
  • .cmake文件可以用作脚本,它运行cmake命令来准备环境预处理或拆分任务,可以在CMakeLists.txt.
  • .cmake文件还可以为项目定义模块。这些项目可以是库的独立构建过程,也可以是复杂的多模块项目的额外方法。

编写Makefile可能比编写 CMake 脚本更难。CMake 脚本在语法和逻辑上与高级语言有相似之处,因此开发人员可以更轻松地创建他们的cmake脚本,并且不会在 Makefile 中迷失方向。

CMake 命令

CMake 命令类似于 C++/Java 方法或函数,它们将参数作为列表并相应地执行某些任务。

CMake 命令不区分大小写。有内置命令,可以从 cmake 文档中找到:https ://cmake.org/cmake/help/latest/manual/cmake-commands.7.html

一些常用的命令

  • message: 打印给定的消息
  • cmake_minimum_required: 设置要使用的 cmake 的最低版本
  • add_executable: 添加具有给定名称的可执行目标
  • add_library:添加要从列出的源文件构建的库目标
  • add_subdirectory: 添加一个子目录来构建

还有一些命令可以让开发人员写出条件语句、循环、迭代列表、赋值:

  • if, endif
  • elif, endif
  • while, endwhile
  • foreach, endforeach
  • list
  • return
  • set_property(为变量赋值。)

缩进不是强制性的,但在编写 CMake 脚本时建议使用。CMake 不使用 ‘;’ 理解语句的结尾。

所有条件语句都应以其相应的结束命令(endif、、endwhileendforeach)结束

CMake 的所有这些属性帮助开发人员编写复杂的构建过程,包括多个模块、库和平台。

例如,KDE 有自己的 CMake 风格指南,如下 URL:

  • https://community.kde.org/Policies/CMake_Coding_Style

CMake 环境变量

环境变量用于为常规构建过程配置编译器标志、链接器标志、测试配置。必须引导编译器搜索给定的库目录。

环境变量的详细列表可以从以下 URL 中看到:

  • https://cmake.org/cmake/help/latest/manual/cmake-env-variables.7.html

一些环境变量被预定义的 CMake 变量覆盖。例如,当定义 CMAKE_CXX_FLAGS 时,会覆盖 CXXFLAGS。

下面是一个示例用例,当您想在编译过程中启用所有警告时,您可以编写-Wall构建命令。如果您使用 CMake 构建代码,则可以-Wall使用 set 命令添加标志。

set(CMAKE_CXX_FLAGS "-Wall")
# append flag, best practice, suggested, don't lose previously defined flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")

CMake 变量

CMake 包括预定义的变量,这些变量默认设置为源树和系统组件的位置。

变量区分大小写,不像命令。您只能在变量定义中使用字母数字字符下划线破折号_, -)。

您可以在以下 URL 中找到有关 CMake 变量的更多详细信息

  • https://cmake.org/cmake/help/v3.0/manual/cmake-language.7.html#variables
  • https://cmake.org/cmake/help/v3.0/manual/cmake-variables.7.html#manual:cmake-variables(7)

一些变量如下所示,这些是根据根文件夹预定义的:

  • CMAKE_BINARY_DIR:构建树顶层和二进制输出文件夹的完整路径,默认定义为构建树顶层。
  • CMAKE_HOME_DIRECTORY: 源树顶部的路径
  • CMAKE_SOURCE_DIR:源树顶层的完整路径。
  • CMAKE_INCLUDE_PATH: 用于查找文件的路径,路径

可以使用 访问变量值${<variable_name>}

message("CXX Standard: ${CMAKE_CXX_STANDARD}")
set(CMAKE_CXX_STANDARD 14)

就像上面的变量一样,您可以定义自己的变量。您可以调用 set 命令为新变量设置值或更改现有变量的值,如下所示:

set(TRIAL_VARIABLE "VALUE")
message("${TRIAL_VARIABLE}")

CMake 列表

CMake 中的所有值都存储为字符串,但在特定上下文中可以将字符串视为列表。

通过连接由半列“;”分隔的元素表示为字符串的元素列表。

set(files a.txt b.txt c.txt)
# sets files to "a.txt;b.txt;c.txt"

为了访问值列表,您可以使用 CMake 的 foreach 命令,如下所示:

foreach(file ${files}) 
message("文件名: ${file}")
endforeach()

CMake 生成器表达式

在构建系统生成期间评估生成器表达式以生成特定于每个构建配置的信息。

Generator expressions are allowed in the context of many target properties, such as LINK_LIBRARIESINCLUDE_DIRECTORIESCOMPILE_DEFINITIONS and others. They may also be used when using commands to populate those properties, such as target_link_libraries()target_include_directories()target_compile_definitions() and others.

https://cmake.org/cmake/help/v3.3/manual/cmake-generator-expressions.7.html

Start Building C++ Code with CMake

In previous sections, we have covered the core principles of writing CMake scripts. Now, we can continue to write actual scripts to start building C++ code.

We can just start with a basic “Hello World!” example with CMake so we wrote the following “Hello CMake!” the main.cpp file as following:

#include <iostream>
int main() {
std::cout<<"Hello CMake!"<<std::endl;
}

Our purpose is to generate a binary to print “Hello CMake!”.

If there is no CMake we can run compiler to generate us a target basically with only following commands.

$ g++ main.cpp -o cmake_hello

CMake helps to generate bash commands with the instructions you gave, for this simple project we can just use a simple CMakeLists.txt which creates Makefile for you to build binary.

It is obvious that, for such a small project it is redundant to use CMake but when things get complicated it will help a lot.

In order to build main.cpp, using add_executable would be enough, however, let’s keep things in order and write it with proper project name and cmake version requirement as below:

cmake_minimum_required(VERSION 3.9.1)project(CMakeHello)add_executable(cmake_hello main.cpp)

When the script ready, you can run cmake command to generate Makefile/s for the project.

You will notice that, cmake is identifying compiler versions and configurations with default information.

$ cmake CMakeLists.txt
-- The C compiler identification is AppleClang 9.0.0.9000039
-- The CXX compiler identification is AppleClang 9.0.0.9000039
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/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 CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/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: /Users/User/Projects/CMakeTutorial

When cmake finishes its job, Makefile will be generated together with CMakeCache.txt and some other artefects about build configuration. You can run make command to build project.

$ make all
#or
$ make cmake_hello

Let’s make things little more complicated.

如果您的代码依赖于 C++14 或更高版本怎么办。如果您查看 C++14 规范,您会看到,C++14 引入了返回类型推导,让我们在 main.cpp 中添加一个新方法,具有自动返回类型,如下所示。

#include <iostream>
auto sum(int a, int b){
        return a + b;
}
int main() {
        std::cout<<"Hello CMake!"<<std::endl;
        std::cout<<"Sum of 3 + 4 :"<<sum(3, 4)<<std::endl;
        return 0;
}

如果您尝试使用默认设置构建上述代码,您可能会收到有关自动返回类型的错误,因为大多数主机默认配置为使用 C++99 配置。因此,您应该将编译器指向使用 C++14 构建,并将 CMAKE_CXX_STANDARD 变量设置为 14。如果您想在命令行中添加 C++14,您可以设置-std=c++14.

CMake 生成的 Makefile 的默认设置大多低于 C++14,因此您应该添加 14 标志,如下所示。

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
add_executable(cmake_hello main.cpp)

现在,您应该能够正确构建它了。(即使您没有添加 14 个标准,cmake 命令也可以工作,但 make 命令会返回错误。)

CMake 为您简化了构建过程;特别是当您进行交叉编译以确保您使用具有正确配置的正确版本的编译器时,这将使多个开发人员能够在包括构建服务器和开发人员 PC 在内的所有机器上使用相同的构建配置。

如果你想为多个平台构建:

  • 分别为 Windows、Mac 和 Linux 生成可执行文件。
  • 为高于 X 的 Linux 内核版本添加不同的宏。

以下变量可用于检查系统相关信息:
粘贴自:https ://cmake.org/Wiki/CMake_Checking_Platform

  • CMAKE_SYSTEM
    完整的系统名称,例如“Linux-2.4.22”、“FreeBSD-5.4-RELEASE”或“Windows 5.1”
  • CMAKE_SYSTEM_NAME
    构建目标系统的名称。三个常用值是 Windows、Darwin 和 Linux,但也存在其他几个值,例如 Android、FreeBSD 和 CrayLinuxEnvironment。没有操作系统的平台,例如嵌入式设备,被赋予通用作为系统名称。
  • CMAKE_SYSTEM_VERSION
    操作系统的版本。一般是内核版本。
  • CMAKE_SYSTEM_PROCESSOR
    处理器名称(例如“Intel(R) Pentium(R) M 处理器 2.00GHz”)
  • CMAKE_HOST_SYSTEM_NAME
    托管构建的系统的名称。具有与 CMAKE_SYSTEM_NAME 相同的可能值。

让我们检查一下构建系统是 Unix 还是 Windows。

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
# UNIX, WIN32, WINRT, CYGWIN, APPLE are environment variables as flags set by default system
if(UNIX)
    message("This is a ${CMAKE_SYSTEM_NAME} system")
elseif(WIN32)
    message("This is a Windows System")
endif()
# or use MATCHES to see if actual system name 
# Darwin is Apple's system name
if(${CMAKE_SYSTEM_NAME} MATCHES Darwin)
    message("This is a ${CMAKE_SYSTEM_NAME} system")
elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows)
    message("This is a Windows System")
endif()
add_executable(cmake_hello main.cpp)

系统信息会检查您的构建系统,而不是构建正确的二进制文件,例如使用宏。您可以定义编译器宏以在构建过程中发送到代码以更改行为。

大型代码库被实现为与系统无关的宏,以便仅将某些方法用于正确的系统。这也可以防止错误,下一节将展示如何定义宏并在代码中使用。

使用 CMake 定义宏

宏帮助工程师根据运行的系统配置有条件地构建代码以丢弃或包含某些方法。

您可以在 CMake 中使用add_definitions命令定义宏,-D在宏名称前使用标志。

让我们定义宏命名CMAKEMACROSAMPLE并在代码中打印它。

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
# or use MATCHES to see if actual system name 
# Darwin is Apple's system name
if(${CMAKE_SYSTEM_NAME} MATCHES Darwin)
    add_definitions(-DCMAKEMACROSAMPLE="Apple MacOS")
elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows)
    add_definitions(-DCMAKEMACROSAMPLE="Windows PC")
endif()
add_executable(cmake_hello main.cpp)

下面是带有打印宏的新 main.cpp。

#include <iostream>
#ifndef CMAKEMACROSAMPLE
    #define CMAKEMACROSAMPLE "NO SYSTEM NAME"
#endif
auto sum(int a, int b){
        return a + b;
}
int main() {
        std::cout<<"Hello CMake!"<<std::endl;
		std::cout<<CMAKEMACROSAMPLE<<std::endl;
        std::cout<<"Sum of 3 + 4 :"<<sum(3, 4)<<std::endl;
        return 0;
}

CMake 文件夹组织

在构建应用程序时,我们尝试保持源代码树的清洁并分离自动生成的文件和二进制文件。

对于 CMake,很多开发者喜欢在根目录下创建一个 build 文件夹,并在里面启动 CMake 命令,如下所示。确保您确实清除了所有以前生成的文件 (CMakeCache.txt),否则它不会在 build.xml 中创建文件。

$ mkdir build
$ cmake ..
$ ls -all
-rw-r--r--   1 onur  staff  13010 Jan 25 18:40 CMakeCache.txt
drwxr-xr-x  15 onur  staff    480 Jan 25 18:40 CMakeFiles
-rw-r--r--   1 onur  staff   4964 Jan 25 18:40 Makefile
-rw-r--r--   1 onur  staff   1256 Jan 25 18:40 cmake_install.cmake
$ make all

上面的命令在构建目录中创建构建文件。它被称为外包构建。

您可以将build/文件夹添加到您的.gitignore以禁用跟踪。

此时,您不能强制使用变量的 CMake 通过设置变量将工件强制创建到另一个文件夹中,因此如果您不想创建目录并cd进入其中,也可以使用以下命令创建文件夹并在其中生成文件它。-H-B标志会有所帮助。

  • -H 指向源树根。
  • -B 指向构建目录。
$ cmake -H. -Bbuild
# H indicates source directory
# B indicates build directory
# For CLion, you can navigate to CLion -> Preferences -> Build, Execution and Deployment -> CMake -> Generation Path

但是,您可以在 CMake 中编写脚本来操作库的位置和可执行文件的位置。

让我们编辑我们CMakeLists.txt以在 bin 文件夹中生成二进制文件,设置为CMAKE_RUNTIME_OUTPUT_DIRECTORYEXECUTABLE_OUTPUT_PATH

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
add_executable(cmake_hello main.cpp)

构建项目后,cmake_hello将在 build/bin 文件夹中生成二进制文件。

可以对库路径以及具有以下变量的共享库(.dll 或 .so)执行相同的过程。

  • LIBRARY_OUTPUT_PATH
  • CMAKE_LIBRARY_OUTPUT_DIRECTORY

或者您可以使用静态库(.a 或 .lib)的存档输出路径

  • CMAKE_ARCHIVE_OUTPUT_DIRECTORY
  • ARCHIVE_OUTPUT_PATH

在某些情况下,设置单个路径就足够了,但对于具有多个模块的大型项目,您可能需要编写 cmake 脚本来管理文件夹以轻松组织构建结构。这将帮助您打包和部署、安装生成的可执行文件、库、带有特定文件夹的文档。

禁用源内构建是一种常见的做法。以下脚本可用于阻止源内构建。

  • OpenCV项目
    脚本来源:https ://github.com/opencv/opencv/blob/master/CMakeLists.txt
# Disable in-source builds to prevent source tree corruption.
if(" ${CMAKE_SOURCE_DIR}" STREQUAL " ${CMAKE_BINARY_DIR}")
  message(FATAL_ERROR "
FATAL: In-source builds are not allowed.
       You should create a separate directory for build files.
")
endif()

使用 CMake 构建库

让我们用额外的子文件夹和一个类来扩展 CMakeHello 项目。

为了简单起见,我们添加了一个模板类,它能够对整数进行 4 种数学运算,即求和、减法、除法、乘法。

  • 首先,我们在源代码树中创建了 lib/math 文件夹。
  • 然后,添加类文件,operations.cpp, operations.hpp如下
#ifndef CMAKEHELLO_OPERATIONS_HPP
#define CMAKEHELLO_OPERATIONS_HPP
namespace math {
    class operations{
    public:
        int sum(const int &a, const int &b);
        int mult(const int &a, const int &b);
        int div(const int &a, const int &b);
        int sub(const int &a, const int &b);
    };
}
#endif //CMAKEHELLO_OPERATIONS_HPP
#include <exception>
#include <stdexcept>
#include <iostream>
#include "operations.hpp"
int math::operations::sum(const int &a, const int &b){
    return a + b;
}
int math::operations::mult(const int &a, const int &b){
    return a * b;
}
int math::operations::div(const int &a, const int &b){
    if(b == 0){
        throw std::overflow_error("Divide by zero exception");
    }
    return a/b;
}
int math::operations::sub(const int &a, const int &b){
    return a - b;
}
  • 然后,我们修改main.cpp为包含 operations.hpp并使用它的sum方法,而不是之前写的 sum。
#include <iostream>
#include "lib/math/operations.hpp"
int main() {
	std::cout<<"Hello CMake!"<<std::endl;
        math::operations op;
        int sum = op.sum(3, 4);
	std::cout<<"Sum of 3 + 4 :"<<sum<<std::endl;
	return 0;
}

快速说明:共享库与静态库

在继续使用 C++ 构建库之前,让我们简要了解一下共享库和静态库。

共享库文件扩展名:

  • Windows:.dll
  • Mac OS X:.dylib
  • Linux:.so

静态库文件扩展名:

  • Windows:.lib
  • Mac OS X:.a
  • Linux:.a

共享库主要放置在主机的共享资源中,以确保多个应用程序可以访问它们。编译器的构建系统假设共享库在执行期间将位于共享文件夹中,因此应用程序二进制大小会减少,它将在执行期间处理来自共享库的一些资源。该要求也会降低性能因为在每次执行时,它都会尝试从共享对象加载指令。

静态库用于通过编译器将指令直接提取到应用程序二进制文件中,因此库中所需的所有代码都已经注入到最终的应用程序二进制文件中。这增加了对象的大小,但增加了二进制文件的大小,但性能得到了提高。使用静态库构建的应用程序也不需要对运行平台的依赖。

用目标建库

如果您只想将这些文件与 main.cpp 一起构建,您可以在 add_executable 命令旁边添加源文件。这将一起编译并创建一个二进制文件。导致 15.076 字节的 exe 文件。

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
add_executable(cmake_hello main.cpp lib/math/operations.cpp lib/math/operations.hpp)

作为替代方案,您可以创建一个名为 ${SOURCES} 的变量作为列表以包含目标源。它可以通过多种不同的方式完成,具体取决于您的方法。

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
set(SOURCES main.cpp 
            lib/math/operations.cpp 
            lib/math/operations.hpp)
add_executable(cmake_hello ${SOURCES})

构建库与目标分开

我们可以将库单独构建为sharedstatic

如果这样做,我们还需要将库链接到可执行文件,以使可执行文件能够从操作库进行调用。我们还想在build 文件夹的lib目录中生成库二进制文件。

  • LIBRARY_OUTPUT_PATH
  • add_library 命令为SHAREDorSTATIC
  • target_link_libraries目标(cmake_hello)
cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_PATH  ${CMAKE_BINARY_DIR}/lib)
message(${CMAKE_BINARY_DIR})
add_library(math SHARED lib/math/operations.cpp)
#add_library(math STATIC lib/math/operations.cpp)
add_executable(cmake_hello main.cpp)
target_link_libraries(cmake_hello math)

您会看到,在 mac OS 中,生成了 libmath.dylib,二进制大小减少到 14.876 字节。(没有重大变化,因为代码已经非常小)。

将库构建为子模块 CMake

另一个库构建过程是,您可以在文件夹中编写一个新CMakeLists.txt文件lib/operations,在构建 exe 文件之前独立构建它。

当生成模块需要可选构建时,最需要这种情况。在我们的例子中,上述解决方案更好,因为它们都依赖。但是,如果您想添加一个案例以仅构建库并跳过可执行构建过程,那么下面的示例也可以工作。

生成一个新的CMakeLists.txt内部lib/math文件夹,如下面的代码片段所示来构建库。

cmake_minimum_required(VERSION 3.9.1)
set(LIBRARY_OUTPUT_PATH  ${CMAKE_BINARY_DIR}/lib)
add_library(math SHARED operations.cpp)
  • 您应该从 main中删除add_library命令和设置LIBRARY_OUTPUT_PATHCMakeLists.txt.
  • 现在,您应该使用add_subdirectoy. 此命令使 cmake 去文件夹并在其中构建CMakeLists.txt它。
cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
message(${CMAKE_BINARY_DIR})
add_subdirectory(lib/math)
add_executable(cmake_hello main.cpp)
target_link_libraries(cmake_hello math)
  • 现在,您将再次在 build/lib 文件夹中看到 libmath.diylib。

上面的示例显示了如何处理源树中的其他源。根据您的发布计划,将它们全部一起构建或单独构建。

使用 CMake 查找现有库

在大多数情况下,您可能需要在构建主机上使用已安装的库。例如,您可以将带有 apt-get 或 brew 包管理器的 boost 库安装到您的系统中。

如果未安装库,CMake 有条件语句允许您阻止构建过程。

如果一个库以其配置安装到系统中.cmake,cmake 将能够查找系统默认库位置以找到该库。喜欢/usr/lib;/usr/local/lib

Boost 库可以通过包管理器、brewapt-get安装到系统。我们可以使用 cmake 的find_package命令在构建可执行文件之前检查库是否存在。

让我们稍微改变一下我们的例子。我想使用 Boost 正态分布库来生成随机样本。因此,我包括boost/random.hpp, 并初始化boost::random::normal_distribution以生成数字variate_generator.

#include <iostream>
#include "lib/math/operations.hpp"
#include <boost/random.hpp>
int main() {
	std::cout<<"Hello CMake!"<<std::endl;
        math::operations op;
        int sum = op.sum(3, 4);
	std::cout<<"Sum of 3 + 4 :"<<sum<<std::endl;
    //Boost Random Sample
    boost::mt19937 rng;
    double mean = 2.3;
    double std = 0.34;
    auto normal_dist = boost::random::normal_distribution<double>(mean, std);
    boost::variate_generator<boost::mt19937&,
            boost::normal_distribution<> > random_generator(rng, normal_dist);
    for(int i = 0; i < 2; i++){
        auto rand_val = random_generator();
        std::cout<<"Random Val "<<i+1<<" :"<<rand_val<<std::endl;
    }
	return 0;
}

CMakeLists.txt现在应该怎么写?它应该检查 Boost 库,如果它可以找到包含标题和链接库。

注意:在大多数情况下,默认库和包含路径是在 cmake 默认配置中定义的,因此它可能会在不做任何修改的情况下找到这些库,但不建议将 CMakeLists.txt 保留为信任系统。

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
#set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
message(${CMAKE_BINARY_DIR})
add_executable(cmake_hello main.cpp)
add_subdirectory(lib/math)
find_package(Boost 1.66)
# Check for libray, if found print message, include dirs and link libraries.
if(Boost_FOUND)
    message("Boost Found")
    include_directories(${Boost_INCLUDE_DIRS})
    target_link_libraries(cmake_hello ${Boost_LIBRARIES})
elseif(NOT Boost_FOUND)
    error("Boost Not Found")
endif()
target_link_libraries(cmake_hello math)

include_directories将包括库头文件,target_link_libraries将链接 boost 库。由于我们定义了错误,如果找不到包,上述代码将不会继续构建,但您也可以REQUIRED在包后添加标志以引发错误。

注意:系统已经识别了 Boost 名称,因为 cmake 已经定义了检查 Boost 库的命令。如果不是像 boost 这样的通用库,你应该编写自己的脚本来启用这个功能。

当找到一个包时,将自动初始化以下变量。

<NAME>_FOUND: 显示是否找到的标志
<NAME>_INCLUDE_DIRS or <NAME>_INCLUDES: 头目录
<NAME>_LIBRARIES or <NAME>_LIBRARIES or <NAME>_LIBS: 库文件
<NAME>_DEFINITIONS

那么,如果库位于自定义文件夹中且位于源代码树之外,会发生什么情况。如果没有任何 CMake,您的命令行构建将如下所示。

g++ main.cpp -o cmake_hello -I/home/onur/libraries/boost/include -L/home/onur/libraries/boost -lBoost

使用此逻辑,您应该使用以下命令添加包含和库文件夹。

include_directories(/Users/User/Projects/libraries/include)
link_directories(/Users/User/Projects/libraries/libs)
# elseif case can be 
elseif(NOT Boost_FOUND)
message("Boost Not Found")
	include_directories(/Users/User/Projects/libraries/include)
	link_directories(/Users/User/Projects/libraries/libs)
	target_link_libraries(cmake_hello Boost)
endif()

在为您的项目包括自定义库的同时,可以遵循许多不同的方法。您还可以编写自定义 cmake 方法来搜索给定文件夹以检查库等。最重要的是了解编译器的链接逻辑。您应该指向标题和库(.so,.a)文件夹和链接库以进行编译过程。

上述情况不是很安全,因为您无法确定它是否存在于主机的文件夹中。最安全的方法是在继续之前获取库的来源并构建。为了提供此属性,应将依赖库与其源一起添加。如果库是封闭源代码,您应该在源代码树中包含二进制文件。

然而,由于许可问题、法律问题等原因,分发源代码并不总是那么容易。构建工程师有责任为这种情况找出最佳实践。

目标系统配置

极有可能为多个系统或在多个系统上构建您的应用程序、库。例如,如果您想在 Intel 系统上部署二进制文件,或者您可能希望为嵌入式系统(如 Android)进行交叉编译,则可能需要包含 Intel 相关库。为了组织构建系统,您应该将所有可能的检查添加到您的 cmake 以及您的代码中作为宏。

更改编译器和链接器以进行构建

假设您将为不同的目标系统交叉编译您的项目。在这种情况下,您的主机系统应包括已安装的目标系统编译器和链接器。CMake的官方文档中显示了一个基本示例:https ://cmake.org/cmake/help/v3.6/manual/cmake-toolchains.7.html#cross-compiling-for-linux

官方的例子对于本文的上下文来说已经足够了,为了基本覆盖它。它是使用其 C/C++ 编译器和工具(链接器等)为 Raspberry Pi 构建的示例

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_SYSROOT /home/devel/rasp-pi-rootfs)
set(CMAKE_STAGING_PREFIX /home/devel/stage)
set(tools /home/devel/gcc-4.7-linaro-rpi-gnueabihf)
set(CMAKE_C_COMPILER ${tools}/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER ${tools}/bin/arm-linux-gnueabihf-g++)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

最重要的部分是正确指向编译器和工具(链接器)路径。Rest 是构建过程的配置,包括优化。

了解目标系统的编译器属性也很重要,它总是与主机系统不同。例如,intel 系统的指令级别与 ARM 不同。甚至 arm cpu 对于指令集也会有所不同,因此对于优化。确保你已经涵盖了这些逻辑来申请你的交叉编译。

在下一节中,我们尝试介绍 GNU GCC 和 Clang 的一些基本编译器和链接器标志。

使用 CMake 的编译器/链接器标志

编译器和链接器标志使工程师能够在构建过程中自定义编译器的行为,以获取警告和构建过程的优化。除了 Clang 或 GNU 编译器使用的标志之外,CMake 没有提供任何新功能。但是,让我们概述一下编译器和链接器标志以及它们的用途。

见,用户手册:

  • https://gcc.gnu.org/onlinedocs/gcc-2.95.2/gcc_2.html
  • https://clang.llvm.org/docs/ClangCommandLineReference.html

编译器标志

设置编译器标志

编译器标志在配置应用程序的最终属性、优化值、编译器行为等方面至关重要。在 Makefile 或命令行中,它可以用不同的方式定义,但在 CMake 中定义它们更容易。就像下面这样:

set(CMAKE_CXX_FLAGS "-std=c++0x -Wall")
# suggested way is to keep previous flags in mind and append new ones
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wall")
# Alternatively, you can use generator expressions, which are conditional expressions. Below says that, if compiper is c++ then set it to c++11
add_compile_options("$<$<STREQUAL:$<TARGET_PROPERTY:LINKER_LANGUAGE>,CXX>:-std=c++11>")

与使用 CMake 设置标志相比,了解这些标志如何工作以及您想要使用哪些标志非常重要。

  • 优化标志设置某些编译器标志或禁用它们。定义这些标志是为了更容易打开/关闭某些优化标志。
  • 请看说明书:
  • https://clang.llvm.org/docs/ClangCommandLineReference.html#optimization-level
  • https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

-O0, -O1, -O2, -O3, -Os, -Oz, -Ofast

  • 警告标志在构建过程中设置警告属性,以在构建过程中查看任何警告以报告它们。有些可能会被关闭以加快编译过程,因为每个警告过程都需要进行分析。参见手册和树。
  • https://gist.github.com/d0k/3608547
  • https://clang.llvm.org/docs/DiagnosticsReference.html

-Wstring-conversion,-Wall,-Wswitch-enum …

设置源文件属性

如果有多个目标,这是 CMake 的一个复杂属性,可能需要更改一个目标的特定行为。如果你想用 C++11 构建 main.cpp,如果你只构建库,你可能想用 C++14 构建它。在这种情况下,您可能希望使用 set_source_files_properties 命令配置某些源的属性,如下所示:

set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/*.cpp PROPERTIES COMPILE_FLAGS "-std=c++11")

从以下手册中可以看到大量属性。

  • https://cmake.org/cmake/help/v3.3/manual/cmake-properties.7.html#source-file-properties

每个都可用于您的目的所需的特定案例。

链接器标志

这是 GNU GCC Linker 的链接器标志列表。

  • https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html

GCC链接器,默认可以搜索环境变量中定义的目录/usr/lib/usr/local/lib, 然后默认链接标准库。

最常见的标志是-l 链接所需的库等-lzlib, -lboost

附加标志,帮助您更改可执行文件的链接选项的行为。

以下是您可以添加链接器标志的变量。

  • CMAKE_EXE_LINKER_FLAGS:链接器在创建可执行文件期间使用的标志
  • CMAKE_EXE_LINKER_FLAGS_RELEASE:链接器在创建发布可执行文件期间使用的标志
  • CMAKE_EXE_LINKER_FLAGS_DEBUG:链接器在创建调试可执行文件期间使用的标志
  • CMAKE_STATIC_LINKER_FLAGS:链接器在创建静态库(.a、.lib)期间使用的标志
  • CMAKE_SHARED_LINKER_FLAGS:链接器在创建静态库(.a、.lib)期间使用的标志
  • CMAKE_MODULE_LINKER_FLAGS:链接器在创建静态库(.a、.lib)期间使用的标志
设置(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
设置(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl")

调试和发布配置

强烈建议根据您的需要创建具有多种配置的 CMakeLists.txt。如果您打算交付二进制文件,您应该制作一个Release不包含二进制调试标志的 . 但是,可执行文件的调试版本包括许多其他标志,这些标志公开内存、方法名称等,以帮助调试器识别错误。交付应用程序的调试版本不是一个好的做法,也不安全。

CMake 可帮助您编写脚本来分离两种类型的输出的最终构建。还有其他构建类型,例如带有调试标志的发布 (RELWITHDEBINFO) 或最小发布大小 (MINSIZEREL)。在此示例中,我们将同时显示两者。

  • 您必须在构建文件夹下为这两种构建类型创建 Debug 和 Release 文件夹。构建/调试和构建/发布。cmake 命令行将更改如下:
$ cmake -H. -Bbuild/Debug
$ cmake -H. -Bbuild/Release
  • 以上配置不足以创建不同的二进制文件。您还应该在命令行上使用 CMAKE_BUILD_TYPE 变量设置构建类型。(CLion 自己处理这个过程。)
$ cmake -DCMAKE_BUILD_TYPE=Debug -H.  -Bbuild/Debug
$ cmake -DCMAKE_BUILD_TYPE=Release -H. -Bbuild/Release
  • CMAKE_BUILD_TYPE 可在 CMakeLists.txt 中访问。您可以在 CMakeLists.txt 中轻松检查构建类型
if(${CMAKE_BUILD_TYPE} MATCHES Debug) 
message("Debug Build")
elseif(${CMAKE_BUILD_TYPE} MATCHES Release)
message("Release Build")
endif()
  • 您还可以使用上一节中显示的配置变量为构建类型分别设置编译器和链接器标志。
  • CMAKE_EXE_LINKER_FLAGS_RELEASE:链接器在创建发布可执行文件期间使用的标志
  • CMAKE_EXE_LINKER_FLAGS_DEBUG:链接器在创建调试可执行文件期间使用的标志
  • CMAKE_CXX_FLAGS_RELEASE
  • CMAKE_CXX_FLAGS_DEBUG

CMake 安装/部署配置

CMake 也有助于为安装过程创建命令,而不仅仅是构建过程。

CMake的安装命令的参数有一个很好的覆盖:

  • https://cmake.org/cmake/help/v3.0/command/install.html

此过程的主要过程是将构建过程生成的文件复制到主机上的目标文件夹。所以:

  • 首先,考虑目标文件夹。CMAKE_INSTALL_PREFIX是定义主机目的地的变量。默认设置为 /usr/local。在构建过程中,您应该在命令行中指向目标文件夹。
$ cmake -DCMAKE_BUILD_TYPE=发布 -DCMAKE_INSTALL_PREFIX=/usr/local/test/with/cmake -H. -Bbuild/发布
  • 请记住,您从终端获取前缀目的地,您应该相应地考虑库和可执行目的地。

CMake 最佳实践

  • 永远记住以前的配置,确保添加新标志而不是覆盖它。最好实现,你的add_flag/remove方法,实现更容易。
set(VARIABLE "${VARIABLE} Flag1 Flag2")
  • 始终仔细检查系统信息,如果无法完成某个配置以防止错误的二进制文件,则会引发错误。
  • 始终检查所需的库以继续构建过程,如果未找到则引发错误。
if(Boost_FOUND) 
message("Boost Found")
else()
error("Boost Not Found")
endif()

资源