CMake 简易教程

本文使用的环境是Windows平台

简介

  CMake 是一中高级编译配置工具,可以用来配置和编译我们的 C/C++ 程序,当然,不仅仅是支持这两种语言。熟悉编译工具的小伙伴肯定听说过 Automake 和 SCons。他们的作用都大同小异。

  CMake 项目的始于1999年,当时开发公司 Kitware 被委托设计一套新的工具来简化研究人员的日常工作软件。目标很明确:提供一组工具,可以在不同平台上配置、构建、测试和部署项目。有关 CMake 更为精彩的叙述,请查阅官方网站www.cmake.org。

  CMake 的所有操作都是通过编译 CMakeLists.txt 来完成的,当多个人用不同的语言或者编译器开发一个项目,最终要输出一个可执行文件或者共享库(dll,so等等)这时候就可以用到 CMake 这个神器,下边我们重点讲解如何根据项目需求,来配置CMakeLists.txt[1]

安装

  这里呢我们就暂时不详细讲解,因为笔者是使用的 Windows 和 VS 2022,安装时候直接选择 上C++桌面开发和C++跨平台开发的组件,CMake 会自动安装。

  找到 VS 自动安装的 cmake.exe 将其添加到环境变量,方便我们直接在cmd窗口调用,一般是在C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin,大家安装位置不一样,自己找一下。如果不会设置环境变量可以看《Windows 设置环境变量》。

入门

这里我们演示一个最简的C++项目使用CMake构建的例子。不使用 VS 2022 ,步骤如下:

  1. 编写一个 C++ 的 hello world
// main.cpp

#include 

int main(){
    std::cout << "hello word" << std::endl;
}
  1. 编写CMakeLists.txt
 #CMakeLists.txt
 
# 最低需要版本 3.8
cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
# 项目名称HELLO ,程序语言为C++
PROJECT (HELLO LANGUAGES CXX) 
# 设置变量,将`main.cpp`用SRC变量替代
SET(SRC main.cpp) 
# 将 SRC 指代的 cpp文件 编译为 hello
ADD_EXECUTABLE(hello ${SRC})
  1. 新建build目录,切换进去,然后输入cmake ..,进行项目构建(生成构建器)
# 当前目录结构如下
├─ build
├─ CMakeLists.txt
└─ main.cpp
PS F:cmake01\build> cmake ..
-- Building for: Visual Studio 17 2022
-- Selecting Windows SDK version 10.0.22000.0 to target Windows 10.0.22621.
-- The CXX compiler identification is MSVC 19.35.32215.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: 
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: F:/cmake01/build

如果一切顺利,项目的配置已经在build目录中生成

  1. 继续输入cmake --build .编译可执行文件:
PS F:\cmake01\build> cmake --build .
MSBuild version 17.5.0+6f08c67f3 for .NET Framework

  Checking Build System
  Building Custom Rule F:/cmake01/CMakeLists.txt
  main.cpp
  hello.vcxproj -> F:\cmake01\build\Debug\hello.exe
  Building Custom Rule F:/BaiduSyncdisk/cmake01/CMakeLists.txt

生成的可执行文件就在 Debug 下。

进阶

如果你想更详细的去学习 CMake 的话,这里推荐您一本更精彩的书籍:《CMake Cookbook中文版》,如果是初学者的话,还是建议阅读完本教程,再去阅读这本书。

CMake 脚本结构

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

cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
PROJECT (HELLO LANGUAGES CXX) 
SET(SRC main.cpp) 
ADD_EXECUTABLE(hello ${SRC})

CMake 中,C++是默认的编程语言。不过,还是建议使用LANGUAGES选项在PROJECT命令中显式地声明项目的语言[1]

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

通过下列命令生成构建器:

$ cd build
$ cmake ..

这里,我们创建了一个目录 build (生成构建器的位置),进入 build 目录,并通过指定 CMakeLists.txt 的位置(本例中位于父目录中)来调用 CMake。

常用关键字介绍[2]

PROJECT:可以用来指定工程的名字和支持的语言,默认支持所有语言

# 指定了工程的名字,并且支持所有语言
PROJECT (HELLO)   
# 指定了工程的名字,并且支持语言是 C++
# PROJECT (HELLO CXX)    
# 指定了工程的名字,并且支持语言是 C 和 C++
# PROJECT (HELLO C CXX)    

SET:用来指定变量的

SET(SRC_LIST main.cpp)    # SRC_LIST 变量就包含了 main.cpp

还可以这样:SET(SRC_LIST main.cpp c1.cpp c2.cpp),含义是SRC_LIST 包含了 main.cpp c1.cpp c2.cpp

ADD_EXECUTABLE:生成可执行文件

ADD_EXECUTABLE(hello ${SRC_LIST})   

生成的可执行文件名是 hello,源文件读取变量 SRC_LIST 中的内容

语法的基本原则[2]

  • 变量使用${ }方式取值
  • 指令(参数1 参数2 …) ,参数使用括弧括起,参数之间使用空格分开,如SET(SRC_LIST main.cpp c1.cpp c2.cpp)
  • 指令是大小写无关的,参数和变量是大小写相关的。不过推荐全部使用大写指令

语法注意事项[2]

  • 如果源文件名中含有空格,就必须要加双引号,如文件main 1.cpp需要写成 SET(SRC_LIST "main 1.cpp")

内部构建和外部构建

  简单的来讲,内部构建就是将 cmake 产生的临时文件都放在了项目源目录下,这样有一个很大的弊端就是产生的临时文件非常多,会破坏项目的结构,也可以讲对项目入侵严重;上边我们使用的其实就是所谓的外部构建,生成的临时文件都放在 build 目录下,有兴趣大家可以分别试一下,反正临时文件多到离谱。

静态库和动态库的构建

  本部分重点是如何分别构建静态库和动态库,以及如何配置 CMakeLists.txt 实现同时构建两种库,会捎带讲一下两个库的区别和优劣

区别 扩展名 说明
静态库 “.a”或“.lib” 编译时会直接整合到目标程序中,编译成功的可执行文件可独立运行
动态库 “.so”或“.dll” 编译时不会放到连接的目标程序中,即可执行文件无法单独运行。

接下来我们试着从源码构建动态和静态库,然后再通过其他程序来实现库调用,来看CMake如何实现项目的配置

任务:

1,建立一个静态库和动态库,提供 HelloFunc 函数供其他程序编程使用,HelloFunc 向终端输出 Hello World 字符串。

2,新建一个项目,使用刚才我们构建的共享库。

一、构建共享库

首先我们新建一个构建库的项目,目录结构如下:

├── build
├── CMakeLists.txt
└── lib
    ├── CMakeLists.txt
    ├── hello.cpp
    └── hello.h

接下来分别介绍每个文件中的内容:

hello.h 中的内容[2]

#ifndef HELLO_H
#define Hello_H

void HelloFunc();

#endif

hello.cpp 中的内容[2]

#include "hello.h"
#include 
void HelloFunc(){
    std::cout << "Hello World" << std::endl;
}

项目根目录下 CMakeLists.txt 的内容[2]

PROJECT(HELLO)
ADD_SUBDIRECTORY(lib bin)

lib 文件夹中 CMakeLists.txt 的内容[2]

SET(LIBHELLO_SRC hello.cpp)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})
CMake 参数详解[2]
  • SET(LIBHELLO_SRC hello.cpp):设置一个变量,名称为LIBHELLO_SRC,指向hello.cpp

    LIBHELLO_SRC:我们定义的变量名称,可以任意定

    hello.cpp:文件hello.cpp

  • ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC}):构建一个名称为hello的动态库

    hello:dll 动态库名

    SHARED:表示是构建动态库

    ${LIBHELLO_SRC} :我们定义的指向源文件的变量,上文就是指 hello.cpp

动态库和静态库的构建区别:

其实就是ADD_LIBRARY()中关键字的区别,动态库就是SHARED,静态库就是STATIC。如果想要构建静态库,只需要替换该关键字即可

库构建演示:
  1. 首先切换到 build 文件夹,命令:cd build
  2. 执行构建命令:cmake ..
  3. 执行编译命令:cmake --build .
动态库构建演示:
cmake01
├─ build
│  ├─ bin
│  │  ├─ CMakeFiles
│  │  ├─ Debug
│  │  │  ├─ hello.dll # 这是我们最终要的动态库
│  │  │  └─ hello.pdb
│  │  └─ hello.dir
│  ├─ CMakeFiles
│  ├─ x64
│  └─ ...
├─ lib
│  ├─ CMakeLists.txt
│  ├─ hello.cpp
│  └─ hello.h
└─ CMakeLists.txt
静态库构建演示:
cmake01
├─ build
│  ├─ bin
│  │  ├─ CMakeFiles
│  │  ├─ Debug
│  │  │  ├─ hello.lib	# 这是我们最终要的静态库
│  │  │  └─ hello.pdb
│  │  ├─ hello.dir
│  ├─ CMakeFiles
│  ├─ x64
│  └─ ...
├─ lib
│  ├─ CMakeLists.txt
│  ├─ hello.cpp
│  └─ hello.h
└─ CMakeLists.txt
同时构建动态库和静态库[2]

在构建的时候,一般我们会想要同时构建动态和静态库,所以我们直觉上首先想到的是这样:

# CMakeLists.txt

ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})
ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})

如果用这种方式,只会构建一个动态库,不会构建出静态库,这是因为 CMake 在构建的时候,如果已经有缓存文件的话,就不会再生成了。

如果要解决这个问题,我们可以曲线救国,先生成两个不一样的库名,再将后生成的库的名字修改成和前一个同样的名字,这里就要用到SET_TARGET_PROPERTIES命令,这条指令可以用来设置输出的名称,那么我们的配置文件就应该改成如下内容:

# lib/CMakeLists.txt

SET(LIBHELLO_SRC hello.cpp)

ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})

//对hello_static的重名为hello
SET_TARGET_PROPERTIES(hello_static PROPERTIES  OUTPUT_NAME "hello")
//cmake 在构建一个新的target 时,会尝试清理掉其他使用这个名字的库,因为,在构建 libhello.so 时, 就会清理掉 libhello.a
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})

SET_TARGET_PROPERTIES(hello PROPERTIES  OUTPUT_NAME "hello")
SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)

这样就可以在 build/bin/Debug/ 下同时生成 hello.dllhello.lib

三、使用外部共享库和头文件

这里演示如何在我们的项目中使用共享库,首先新建一个项目,目录结构如下:

cmake02
├─ build
├─ include
│  └─ hello.h 	# 将之前使用的 hello.h 复制过来
├─ libs
│  ├─ hello.lib	# 将生成的 hello.lib 复制进来
│  └─ hello.dll	# 将生成的 hello.dll 复制进来
├─ CMakeLists.txt
└─ main.cpp
各个文件的内容:

main.cpp 内容:

#include 

int main(){
	HelloFunc();
}

CMakeLists.txt 内容:

cmake_minimum_required(VERSION 3.8 FATAL_ERROR)

PROJECT(HELLO)

# 设置头文件路径:
INCLUDE_DIRECTORIES("${PROJECT_SOURCE_DIR}/include")

LINK_DIRECTORIES(${PROJECT_SOURCE_DIR}/lib)
# 生成可执行文件
ADD_EXECUTABLE(${PROJECT_NAME} main.cpp)
# 链接
TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${PROJECT_SOURCE_DIR}/lib/hello.lib)
构建编译生成可执行文件
  1. 首先切换到 build 文件夹,命令:cd build
  2. 执行构建命令:cmake ..
  3. 执行编译命令:cmake --build .
cmake02
├─ build
│  ├─ CMakeFiles
│  ├─ Debug
│  │  ├─ HELLO.exe	# 这就是生成的可执行文件,可以直接运行
│  │  └─ HELLO.pdb
│  ├─ HELLO.dir
│  └─ x64
├─ include
│  └─ hello.h
├─ lib
│  ├─ hello.lib
│  └─ hello.dll
├─ CMakeLists.txt
└─ main.cpp

这里其实使用的是hello.libhello.dll没有用到,因为我还不会,等我学会了补充

四、动态调用和静态调用的优劣

关于应用程序是选择调用dll(动态调用)还是调用lib文件(静态调用),这里简单讲一下我的理解:

应用场景 动态调用 静态调用 说明
增量升级 方便 不方便 因为dll是和exe分开的,可以只更新dll
链式调用 可以 不可以 可以在dll中再引用其他的dll,但是静态调用不支持

参考引用:

  1. 陈晓伟.《CMake菜谱(CMake Cookbook中文版)》[EB/OL].2020-02-13/2023-03-25.
  2. 刘贝斯.《从零开始详细介绍CMake_哔哩哔哩_bilibili》[Z/OL].2022-01-04/2023-03-25.

你可能感兴趣的:(windows,c++,CMAKE)