本文进行Cmake学习,借鉴了相应的文档前言 · GitBook

CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的makefile或者project文件,CMake 的组态档取名为 CMakeLists.txt。也就是在CMakeLists.txt这个文件中写cmake代码。 一句话:cmake就是将多个cpp、hpp文件组合构建为一个大工程的语言。

本cmake系列介绍

首先大家学习cmake一定会遇到《cmake practice》这本书,但是纯粹讲理论,如果没有实践的话是学不会的。learning by doing是学习各种知识的不二法门。

另外,在不太大的工程中,其实cmake也不需要钻研到多深入,日后工作用得多了自然就会了。比如说PRIVATE、INTERFACE、PUBLIC这三个参数在实际中的用途,当时请教了wps的一位前辈,最终的理解在这篇文章中cmake-scope。他自己也是偶尔有一次用这三个参数测试了一下,其他时候不怎么关心。

demo1 hello-cmake

文件树

├── CMakeLists.txt

├── main.cpp

main.cpp

#include <iostream>
int main(int argc, char const *argv[])
{
    std::cout << " hello  Cmake!" << std::endl;
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
# 设置最小的cmake版本
project(hello_cmake)
# 项目工程名
add_executable(hello_cmake main.cpp)
# 设置对应的可执行文件

解析

命令作用解析

project(hello_cmake)
# 项目工程名

CMake构建包含一个项目名称,上面的命令会自动生成一些变量,在使用多个项目时引用某些变量会更加容易。

比如生成了: PROJECT_NAME 这个变量PROJECT_NAME变量名,${PROJECT_NAME}是变量值,值为hello_cmake

add_executable(hello_cmake main.cpp) # 设置对应的可执行文件

add_executable()命令指定某些源文件生成可执行文件,本例中是main.cpp。

add_executable()函数的第一个参数是可执行文件名,第二个参数是要编译的源文件列表

外部构建与内部构建

变量CMAKE_BINARY_DIR指向 cmake命令的根文件夹,所有二进制文件在这个文件夹里产生。

使用外部构建,我们可以创建一个可以位于文件系统上任何位置的构建文件夹。 所有临时构建和目标文件都位于此目录中,以保持源代码树的整洁。

运行下述代码,新建build构建文件夹,并运行cmake命令

mkdir build
cd build/
cmake ..

demo2  hello-headers

文件树

├── CMakeLists.txt
├── include
│   └── Hello.h
└── src
    ├── Hello.cpp
    └── main.cpp

Hello.h

#ifndef _HELLO_
#define _HELLO_
class Hello{
    public:
        void print();
};

#endif // !_HELLO_

Hello.cpp

#include<iostream>
#include "Hello.hpp"
void Hello::print(){
    std::cout << "Hello Header" << std::endl;
}

main.cpp

#include "Hello.hpp"
int main(int argc, char const *argv[])
{
    Hello hello;
    hello.print();
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(hello_header)
set(SOURCE 
    src/Hello.cpp 
    src/main.cpp
)
#创建一个变量,名字叫做SOURCE,包含了所有的cpp文件

add_executable(${PROJECT_NAME} ${SOURCE})
#添加对应的可执行文件

target_include_directories(${PROJECT_NAME}
    PRIVATE
    ${PROJECT_SOURCE_DIR}/include
)
#添加对应的头文件

CMake的各种可用变量

CMake语法指定了许多变量,可用于帮助您在项目或源代码树中找到有用的目录。 其中一些包括:

demo3 Static Library

本文自己创建库的操作,应该暂时用不到。但是关于如何添加路径,链接库的命令,还是需要掌握的。

 文件树

├── CMakeLists.txt
├── include
│   └── static
│       └── Hello.h
└── src
    ├── Hello.cpp
    └── main.cpp

Hello.h

/*声明了Hello类,Hello的方法是print(),*/

#ifndef __HELLO_H__
#define __HELLO_H__

class Hello
{
public:
    void print();
};

#endif

Hello.cpp

/*实现了Hello::print()*/
#include <iostream>

#include "static/Hello.h"

void Hello::print()
{
    std::cout << "Hello Static Library!" << std::endl;
}

main.cpp

#include "static/Hello.h"

int main(int argc, char *argv[])
{
    Hello hi;
    hi.print();
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(hello_librarys)


#创建对应的静态链接库
add_library(hello_librarys STATIC src/Hello.cpp)

# 添加对应的头文件
target_include_directories(hello_librarys 
    PUBLIC
    ${PROJECT_SOURCE_DIR}/include/static
)

# 设置对应的可执行文件
add_executable(hello_library src/main.cpp)

# 链接可执行文件和静态链接库
target_link_libraries(hello_library
    PRIVATE
    hello_librarys
)

CMake解析

add_library()函数用于从某些源文件创建一个库,默认生成在构建文件夹。 写法如下:

add_library(hello_library STATIC
    src/Hello.cpp
)

在add_library调用中包含了源文件,用于创建名称为libhello_library.a的静态库。

添加头文件所在的目录

使用target_include_directories()添加了一个目录,这个目录是库所包含的头文件的目录,并设置库属性为PUBLIC

target_include_directories(hello_library
    PUBLIC
        ${PROJECT_SOURCE_DIR}/include
)

正确理解:

  • PRIVATE - 目录被添加到目标(库)的包含路径中
  • INTERFACE - 目录没有被添加到目标(库)的包含路径中,而是链接了这个库的其他目标(库或者可执行程序)包含路径中
  • PUBLIC - 目录既被添加到目标(库)的包含路径中,同时添加到了链接了这个库的其他目标(库或者可执行程序)的包含路径中

也就是说,根据库是否包含这个路径,以及调用了这个库的其他目标是否包含这个路径,可以分为三种scope。

链接库

创建将使用这个库的可执行文件时,必须告知编译器需要用到这个库。 可以使用target_link_library()函数完成此操作。add_executable()连接源文件,target_link_libraries()连接库文件。

add_executable(hello_binary
    src/main.cpp
)

target_link_libraries( hello_binary
    PRIVATE
        hello_library
)

这告诉CMake在链接期间将hello_library链接到hello_binary可执行文件。 同时,这个被链接的库如果有INTERFACE或者PUBLIC属性的包含目录,那么,这个包含目录也会被传递( propagate )给这个可执行文件。

demo4 Shared Library

本文自己创建动态库的操作,应该暂时用不到。但是关于如何添加路径,链接库的命令,还是需要掌握的。

文件树

├── CMakeLists.txt
├── include
│   └── shared
│       └── Hello.h
└── src
    ├── Hello.cpp
    └── main.cpp

Hello.h

/*声明了Hello类,Hello的方法是print(),*/
#ifndef __HELLO_H__
#define __HELLO_H__

class Hello
{
public:
    void print();
};

#endif

Hello.cpp

/*实现了Hello::print()*/
#include <iostream>

#include "shared/Hello.h"

void Hello::print()
{
    std::cout << "Hello Shared Library!" << std::endl;
}

main.cpp

#include "shared/Hello.h"

int main(int argc, char *argv[])
{
    Hello hi;
    hi.print();
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(hello_library)

add_library(hello_library SHARED
    src/Hello.cpp
)
#为这个库目标,添加头文件路径,PUBLIC表示包含了这个库的目标也会包含这个路径
target_include_directories(hello_library
    PUBLIC
    ${PROJECT_SOURCE_DIR}/include

)

add_executable(hello src/main.cpp)

target_link_libraries(hello
    hello_library

)

CMake解析

add_library()函数用于从某些源文件创建一个动态库,默认生成在构建文件夹。 写法如下:

add_library(hello_library SHARED
    src/Hello.cpp
)

demo5 build-type

Cmake的编辑类型

文件树

├── CMakeLists.txt
├── main.cpp

main.cpp

#include <iostream>

int main(int argc, char *argv[])
{
   std::cout << "Hello Build Type!" << std::endl;
   return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  #在命令行中输出message里的信息
  message("Setting build type to 'RelWithDebInfo' as none was specified.")
  #不管CACHE里有没有设置过CMAKE_BUILD_TYPE这个变量,都强制赋值这个值为RelWithDebInfo
  set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE)

  # 当使用cmake-gui的时候,设置构建级别的四个可选项
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
    "MinSizeRel" "RelWithDebInfo")
endif()
project (build_type)
add_executable(cmake_examples_build_type src/main.cpp)

CMake解析

 构建级别

CMake具有许多内置的构建配置,可用于编译工程。 这些配置指定了代码优化的级别,以及调试信息是否包含在二进制文件中。

这些优化级别,主要有:

  • Release —— 不可以打断点调试,程序开发完成后发行使用的版本,占的体积小。 它对代码做了优化,因此速度会非常快,

    在编译器中使用命令: -O3 -DNDEBUG 可选择此版本。

  • Debug ——调试的版本,体积大。

    在编译器中使用命令: -g 可选择此版本。

  • MinSizeRel—— 最小体积版本

    在编译器中使用命令:-Os -DNDEBUG可选择此版本。

  • RelWithDebInfo—— 既优化又能调试。

    在编译器中使用命令:-O2 -g -DNDEBUG可选择此版本。

在命令行运行CMake的时候, 使用cmake命令行的-D选项配置编译类型

cmake .. -D CMAKE_BUILD_TYPE=Release

set()命令

该命令可以为普通变量、缓存变量、环境变量赋值。

处可以设置零个或多个参数。多个参数将以分号分隔的列表形式加入,以形成要设置的实际变量值。零参数将导致未设置普通变量。见unset() 命令显式取消设置变量。

所以此处学习SET命令需要分为设置普通变量,缓存变量以及环境变量三种类别来学习

正常变量

set(<variable> <value>... [PARENT_SCOPE])

设置的变量值 作用域属于整个 CMakeLists.txt 文件。(一个工程可能有多个CMakeLists.txt)

当这个语句中加入PARENT_SCOPE后表示要设置的变量是父目录中的CMakeLists.txt设置的变量。

比如有如下目录树:

├── CMakeLists.txt
└── src
    └── CMakeLists.txt

并且在 顶层的CMakeLists.txt中包含了src目录:add_subdirectory(src)

那么,顶层的CMakeLists.txt就是父目录,

如果父目录中有变量Bang,在子目录中可以直接使用(比如用message输出Bang,值是父目录中设置的值)并且利用set()修改该变量Bang的值,但是如果希望在出去该子CMakeLists.txt对该变量做出的修改能够得到保留,那么就需要在set()命令中加入Parent scope这个变量。当然,如果父目录中本身没有这个变量,子目录中仍然使用了parent scope,那么出了这个作用域后,该变量仍然不会存在。

这里举一个实际的例子:

test:
    build
    sub:
        build
        CmakeLists.txt
    CmakeLists.txt

我们建立一个项目结构如上:

# test/sub/CMakeLists.txt
cmake_minimum_required (VERSION 3.5)
project (subtest)

set (val sub_hello)
set (val par_hello PARENT_SCOPE)

message (">>>>>> in sub level, value = ${val}")
# test/CMakeLists.txt
cmake_minimum_required (VERSION 3.5)
project (partest)

add_subdirectory (sub)

message (">>> in parent , value = ${val}")

执行如下:

#在项目test/build下执行cmake ..
>>>>>> in sub level, value = sub_hello
>>> in parent , value = par_hello
#在项目test/sub/build下执行cmake ..
>>>>>> in sub level, value = sub_hello

从这里来看我们发现在执行父级CmakeLists.txt的内容时,会输出子目录的内容,而在执行子目录的CmakeLists.txt时则只会输出自己的内容。

demo6 Compile Flags

首先说一下什么是编译标志(或者 叫编译选项)。可执行文件的生成离不开编译和链接,那么如何编译,比如编译时使用C++的哪一个标准?这些编译设置都在CMAKE_CXX_FLAGS变量中。(C语言编译选项是CMAKE_C_FLAGS

 文件树

├── CMakeLists.txt
├── main.cpp

main.cpp

#include <iostream>

int main(int argc, char *argv[])
{
   std::cout << "Hello Compile Flags!" << std::endl;

   // only print if compile flag set
#ifdef EX2
  std::cout << "Hello Compile Flag EX2!" << std::endl;
#endif

#ifdef EX3
  std::cout << "Hello Compile Flag EX3!" << std::endl;
#endif

   return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
#强制设置默认C++编译标志变量为缓存变量,如CMake(五) build type所说,该缓存变量被定义在文件中,相当于全局变量,源文件中也可以使用这个变量
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)

project (compile_flags)

add_executable(cmake_examples_compile_flags main.cpp)
#为可执行文件添加私有编译定义
target_compile_definitions(cmake_examples_compile_flags 
    PRIVATE EX3
)
#命令的具体解释在二  CMake解析中,这里的注释只说明注释后每一句的作用

CMake解析

设置每个目标编译标志

在现代CMake中设置C ++标志的推荐方法是专门针对某个目标(target)设置标志,可以通过target_compile_definitions()函数设置某个目标的编译标志

target_compile_definitions(cmake_examples_compile_flags
    PRIVATE EX3
)

如果这个目标是一个库(cmake_examples_compile_flags),编译器在编译目标时添加定义-DEX3 ,并且选择了范围PUBLIC或INTERFACE,该定义-DEX3也将包含在链接此目标(cmake_examples_compile_flags)的所有可执行文件中。 注意,本语句使用了PRIVATE,所以编译选项不会传递。

对于编译器选项,还可以使用target_compile_options()函数。

target_compile_options(<target> [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

是给 target 添加编译选项, target 指的是由 add_executable()产生的可执行文件或 add_library()添加进来的库。<INTERFACE|PUBLIC|PRIVATE>指的是[items...] 选项可以传播的范围, PUBLIC and INTERFACE会传播 <target>的 INTERFACE_COMPILE_DEFINITIONS 属性, PRIVATE and PUBLIC 会传播 target 的 COMPILE_DEFINITIONS 属性。

设置默认编译标志

默认的CMAKE_CXX_FLAGS为空或包含适用于构建类型的标志。 要设置其他默认编译标志,如下使用:

set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)

强制设置默认C++编译标志变量为缓存变量,如CMake(五) build type所说,该缓存变量被定义在文件中,相当于全局变量,源文件中也可以使用这个变量。这个变量原本包含的参数仍然存在,只是添加了EX2。

CACHE STRING "Set C++ Compiler Flags" FORCE命令是为了强制将CMAKE_CXX_FLAGS变量 放到CMakeCache.txt文件中

"${CMAKE_CXX_FLAGS} -DEX2"这个字符串可以保留原有的CMAKE_CXX_FLAGS中的参数,额外添加了一个EX2参数。注意写法:空格,并且参数前加了-D

类似设置CMAKE_CXX_FLAGS,还可以设置其他选项:

  • 设置C编译标志: CMAKE_C_FLAGS
  • 设置链接标志:CMAKE_LINKER_FLAGS.

 demo7 Including Third Party Library

文件树

├── CMakeLists.txt
├── main.cpp

main.cpp

#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/filesystem.hpp>
/*Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,由Boost社区组织开发、
维护。Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能。
*/
int main(int argc, char *argv[])
{
    std::cout << "Hello Third Party Include!" << std::endl;

    // use a shared ptr
    boost::shared_ptr<int> isp(new int(4));

    // trivial use of boost filesystem
    boost::filesystem::path path = "/usr/share/cmake/modules";
    if(path.is_relative())
    {
        std::cout << "Path is relative" << std::endl;
    }
    else
    {
        std::cout << "Path is not relative" << std::endl;
    }

   return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

# Set the project name
project (third_party_include)
# find a boost install with the libraries filesystem and system
#使用库文件系统和系统查找boost install
find_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem system)
#这是第三方库,而不是自己生成的静态动态库
# check if boost was found
if(Boost_FOUND)
    message ("boost found")
else()
    message (FATAL_ERROR "Cannot find Boost")
endif()

# Add an executable
add_executable(third_party_include main.cpp)

# link against the boost libraries
target_link_libraries( third_party_include
    PRIVATE
        Boost::filesystem
)

CMake解析

几乎所有不平凡的项目都将要求包含第三方库,头文件或程序。 CMake支持使用find_package()函数查找这些工具的路径。 这将从CMAKE_MODULE_PATH中的文件夹列表中搜索格式为“ FindXXX.cmake”的CMake模块。 在linux上,默认搜索路径将是/ usr / share / cmake / Modules。 在我的系统上,这包括对大约142个通用第三方库的支持。

此示例要求将Boost库安装在默认系统位置。

Finding a Package

如上所述,find_package()函数将从CMAKE_MODULE_PATH中的文件夹列表中搜索“ FindXXX.cmake”中的CMake模块。 find_package参数的确切格式取决于要查找的模块。 这通常记录在FindXXX.cmake文件的顶部。

find_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem system)

参数:

Boost-库名称。 这是用于查找模块文件FindBoost.cmake的一部分

1.46.1 - 需要的boost库最低版本

REQUIRED - 告诉模块这是必需的,如果找不到会报错

COMPONENTS - 要查找的库列表。从后面的参数代表的库里找boost

可以使用更多参数,也可以使用其他变量。 在后面的示例中提供了更复杂的设置。

Checking if the package is found

大多数被包含的包将设置变量XXX_FOUND,该变量可用于检查软件包在系统上是否可用。

在此示例中,变量为Boost_FOUND:

if(Boost_FOUND)
    message ("boost found")
    include_directories(${Boost_INCLUDE_DIRS})
else()
    message (FATAL_ERROR "Cannot find Boost")
endif()

Exported Variables

找到包后,它会自动导出变量,这些变量可以通知用户在哪里可以找到库,头文件或可执行文件。 与XXX_FOUND变量类似,它们与包绑定在一起,通常记录在FindXXX.cmake文件的顶部。

本例中的变量

Boost_INCLUDE_DIRS - boost头文件的路径

Alias / Imported targets别名/导入目标

大多数modern CMake库在其模块文件中导出别名目标。 导入目标的好处是它们也可以填充包含目录和链接的库。 例如,从CMake v3.5开始,Boost模块支持此功能。 与使用自己的别名目标相似,模块中的别名可以使引用找到的目标变得更加容易。 对于Boost,所有目标均使用Boost ::标识符,然后使用子系统名称导出。 例如,您可以使用:

  • Boost::boost for header only libraries
  • Boost::system for the boost system library.
  • Boost::filesystem for filesystem library.

    与您自己的目标一样,这些目标包括它们的依赖关系,因此与Boost :: filesystem链接将自动添加Boost :: boost和Boost :: system依赖关系。

    要链接到导入的目标,可以使用以下命令:

target_link_libraries( third_party_include
      PRIVATE
          Boost::filesystem
  )

Non-alias targets

尽管大多数现代库都使用导入的目标,但并非所有模块都已更新。 如果未更新库,则通常会发现以下可用变量:

  • xxx_INCLUDE_DIRS - 指向库的包含目录的变量

  • xxx_LIBRARY - 指向库路径的变量

然后可以将它们添加到您的target_include_directories和target_link_libraries中,如下所示:

  # Include the boost headers
  target_include_directories( third_party_include
      PRIVATE ${Boost_INCLUDE_DIRS}
  )

  # link against the boost libraries
  target_link_libraries( third_party_include
      PRIVATE
      ${Boost_SYSTEM_LIBRARY}
      ${Boost_FILESYSTEM_LIBRARY}
  )

许多大型项目由不同的库和二进制文件组成。本文利用多个CMakeLists.txt文件组织这些库和文件。

包括的示例是

  • basic - 此基本示例包括一个静态库,一个仅有头文件的库和一个可执行文件。

本示例说明如何包含子项目。 顶级CMakeLists.txt调用子目录中的CMakeLists.txt来创建以下内容:

  • sublibrary1 - 一个静态库

  • sublibrary2 - 只有头文件的库

  • subbinary - 一个可执行文件

文件树如下:

$ tree
.
├── CMakeLists.txt
├── subbinary
│   ├── CMakeLists.txt
│   └── main.cpp
├── sublibrary1
│   ├── CMakeLists.txt
│   ├── include
│   │   └── sublib1
│   │       └── sublib1.h
│   └── src
│       └── sublib1.cpp
└── sublibrary2
    ├── CMakeLists.txt
    └── include
        └── sublib2
            └── sublib2.h

[提示]

在此示例中,我已将头文件移至每个项目include目录下的子文件夹,而将目标include保留为根include文件夹。 这是防止文件名冲突的一个好主意,因为您必须包括以下文件:

#include "sublib1/sublib1.h"

如果您为其他用户安装库,则默认安装位置为/usr/local/include/sublib1/sublib1.h。

概念

添加子目录

CMakeLists.txt文件可以包含和调用包含CMakeLists.txt文件的子目录。

add_subdirectory(sublibrary1)
add_subdirectory(sublibrary2)
add_subdirectory(subbinary)

引用子项目目录

使用project()命令创建项目时,CMake将自动创建许多变量,这些变量可用于引用有关该项目的详细信息。 这些变量然后可以由其他子项目或主项目使用。 例如,要引用您可以使用的其他项目的源目录。

    ${sublibrary1_SOURCE_DIR}
    ${sublibrary2_SOURCE_DIR}

CMake中有一些变量会自动创建:

Header only Libraries

如果您有一个库被创建为仅头文件的库,则cmake支持INTERFACE目标,以允许创建没有任何构建输出的目标。 可以从here找到更多详细信息

add_library(${PROJECT_NAME} INTERFACE)

创建目标时,您还可以使用INTERFACE范围包含该目标的目录。 INTERFACE范围用于制定在链接此目标的任何库中使用的目标需求,但在目标本身的编译中不使用。

target_include_directories(${PROJECT_NAME}
    INTERFACE
        ${PROJECT_SOURCE_DIR}/include
)

引用子项目中的库

如果子项目创建了一个库,则其他项目可以通过在target_link_libraries()命令中调用该项目的名称来引用该库。 这意味着您不必引用新库的完整路径,而是将其添加为依赖项。

target_link_libraries(subbinary
    PUBLIC
        sublibrary1
)

或者,您可以创建一个别名目标,该目标允许您在上下文(其实就是某个目标的绰号)中引用该目标。

add_library(sublibrary2)
add_library(sub::lib2 ALIAS sublibrary2)

To reference the alias, just it as follows:

target_link_libraries(subbinary
    sub::lib2
)

包含子项目中的目录

从cmake v3开始从子项目添加库时,无需将项目include目录添加到二进制文件的include目录中。

创建库时,这由target_include_directories()命令中的作用域控制。 在此示例中,因为子二进制可执行文件链接了sublibrary1和sublibrary2库,所以当它们与库的PUBLIC和INTERFACE范围一起导出时,它将自动包含$ {sublibrary1_SOURCE_DIR} / inc和$ {sublibrary2_SOURCE_DIR} / inc文件夹。(这个地方设及到了PUBLIC和INTERFACE的使用,本电子书的CMake-scope是讲这个的)

CMake中常使用的变量

EXECUTABLE_OUTPUT_PATH 可执行程序的输出路径
LIBRARY_OUTPUT_PATH 库文件的输出路径

05-19 17:13