CMake入门

CMake是个一个开源的跨平台自动化建构系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录、多个应用程序与多个库。 它用配置文件控制建构过程(build process)的方式和Unix的make相似,只是CMake的配置文件取名为CMakeLists.txt。CMake并不直接建构出最终的软件,而是产生标准的建构档(如Unix的Makefile或Windows Visual C++的projects/workspaces),然后再依一般的建构方式使用。这使得熟悉某个集成开发环境(IDE)的开发者可以用标准的方式建构他的软件,这种可以使用各平台的原生建构系统的能力是CMake和SCons等其他类似系统的区别之处。 CMake配置文件(CMakeLists.txt)可设置源代码或目标程序库的路径、产生适配器(wrapper)、还可以用任意的顺序建构可执行文件。CMake支持in-place建构(二进档和源代码在同一个目录树中)和out-of-place建构(二进档在别的目录里),因此可以很容易从同一个源代码目录树中建构出多个二进档。CMake也支持静态与动态程序库的建构。

“CMake”这个名字是”Cross platform Make”的缩写。虽然名字中含有”make”,但是CMake和Unix上常见的“make”系统是分开的,而且更为高端。 它可与原生建置环境结合使用,例如:make、ninja、苹果的Xcode与微软的Visual Studio。

第一个工程

我们先简单的构建一个Hello World

/************main.c****************/
#include <stdio.h>
int main() {
  printf("hello world!\n");
  return 0;
}

写一个CMake

#CMakeLists.txt
# 定义最低版本
cmake_minimum_required(VERSION 3.4.1)

# 定义工程名
project(main)

# 定义目标名和源文件
add_executable(main main.c)

在构建的时候,我们习惯于将构建产物放到一个独立的目录中去,这个目录一般命名为build

$ mkdir build && cd build
$ cmake .. # 生成makefile
$ make     # 使用make命令生成目标文件, 也可以用cmake命令间接调用: cmake --build .

到这里我们完成了第一个工程的构建。 在这个构建过程,我们使用了三个内置的方法来定义我们的工程

这些方法的定义都可以在cmake的官方文档中找到

多文件工程的代码组织

一般工程不会只有一个文件,对于多文件的工程,最简单的是在add_executable中添加多个文件

...
add_executable(main main.c add.c)

或者更一般的使用正则

...
add_executable(main *.c )

或者我们一般将源码放在src目录下,然后使用file命名组织源码

...
file(GLOB_RECURSE SRC_CORE src/*.c)
add_executable(main ${SRC_CORE})

添加头文件

..
target_include_directories(main PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
...

同时我们还可以根据不同的平台来过滤编译的代码

...
# 非32位系统剔除掉dalvik, 这是android构建中用到的
if(NOT ${CMAKE_ANDROID_ARCH_ABI} STREQUAL "armeabi-v7a")
    list(FILTER SRC_CORE EXCLUDE REGEX "${PROJECT_SOURCE_DIR}/src/dalvik/.*")
endif()

添加编译选项

我们在CMakeLists中添加对未使用变量的警告

set(CMAKE_C_FLAGS "-Wunused-variable)

set函数用于设置变量,可以是自定义的变量,也可以是环境变量。环境变量 我们用到的主要有CMAKE__FLAGS

用于设置安装目录的CMAKE_PREFIX_PATH

参考官方文档

构建library

构建library使用add_library命令。 我们在src目录下创建一个minus的目录,用于存放libmunus.so的代码的构建结果。同时创建以下三个文件

// src/minus/minus.h
#ifndef ADD_H
#define ADD_H
int minus(int a, int b);
#endif
// src/minus/minus.c
#include "minus.h"
int minus(int a, int b) {
  return a - b;
}

# src/minus/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(libminus)
add_library(minus SHARED minus.c)

接着按照习惯在minus目录下创建build目录进行构建

$ mkdir build && cd build
$ cmake .. # 生成makefile
$ make     # 使用make命令生成目标文件, 也可以用cmake命令间接调用: cmake --build .

我们可以在build目录里看到libminus.so

如果是macos应该是看到libminus.dylib

用源码的形式使用动态库

接下来我们要用子工程的方式引用上节的动态库。使用add_subdirectory来使用这个动态库

# src/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)

project(main)

file(GLOB_RECURSE SRC_CORE src/*.c)
# 我们需要过滤掉minus目录下的文件,那是libminus.so的源码文件
list(FILTER SRC_CORE EXCLUDE REGEX "${PROJECT_SOURCE_DIR}/src/minus/.*")

# 添加子依赖
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/minus)

add_executable(main ${SRC_CORE})

# 添加Library
target_link_libraries(main minus)

# 添加头文件查找目录
target_include_directories(main
	PUBLIC
	target_include_directories(main PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
	${CMAKE_CURRENT_SOURCE_DIR}/src/minus)

更改main.c的源码,测试动态库是否有用

#include <stdio.h>
#include "add.h"
#include "minus.h"
int main(){
  printf("1+2=%d\n", add(1, 2));
  printf("2-1=%d\n", minus(1, 2));
  return 0;
}

最后在根目录的build目录里执行构建,并执行构建结果

  1. 有的文档里可能会用其他的函数引入动态库。我们这里使用的库是3.x引入的官方推荐的方式
  1. 添加头文件查找目录可以用其他方式代替: 在minus目录下的CMakeLists.txt里增加头文件的定义
     # 这里INTERFACE的意思是当前这个动态库不需要使用到,但是使用这个动态库的需要
     target_include_directories(minus
           INTERFACE
           ${CMAKE_CURRENT_SOURCE_DIR}
    )
    

    然后根目录的CMakeLists.txt里的target_include_directories声明的${CMAKE_CURRENT_SOURCE_DIR}/src/minus就可以不要了。

添加预编译动态库

下一步我们将在外部先编译一个外部库,然后引入这个外部库。假设我们已经编译好这个外部库,并将头文件和动态库文件放到根目录下的libsinclude

├── include
│   └── multiple.h
├── libs
│   └── libmultiple.so

并在CMakeLists.txt中添加依赖


# 添加multiple动态库的引入
add_library( multiple
             SHARED
             IMPORTED
		)

# 声明动态库的位置和头文件的位置
set_target_properties( # Specifies the target library.
             multiple

             PROPERTIES

             IMPORTED_LOCATION # Provides the path to the library you want to import.

             ${CMAKE_SOURCE_DIR}/libs/libmultiple.so

             INTERFACE_INCLUDE_DIRECTORIES

             ${CMAKE_SOURCE_DIR}/include

             )

add_executable(main ${SRC_CORE})

# 添加依赖
target_link_libraries(main minus multiple)

写完构建脚本后,我们再在main.c中添加测试代码

#include <stdio.h>
#include "minus/minus.h"
#include "add.h"
#include "multiple.h"

int main(){
  printf("1+2=%d\n", add(1, 2));
  printf("2-1=%d\n", minus(2, 1));
  printf("2*2=%ld\n", multiple(2, 2));
  return 0;
}

在根目录中的build子目录中执行构建,并执行结果

➜  build git:(master) ./main
1+2=3
2-1=1
2*2=4

如果是其他系统,预构建的动态库需要做替换,我这边就不写多平台的实现了。留给你们做练习

以源码方式添加第三方动态库

一般源码方式的第三方的动态库都是以压缩包或者git仓库的方式存在的,使用方式大概有几种

第一种方式更新源码比较麻烦,无法进行快速切换; 第二种方式使用过程中感觉比较不好不好使用。多层级git仓库的缺点网上有相关的讨论,可以自行去查找 第三种方式在构建过程中采取下载,配置文件比较不好写 我们这里采用第四种方式

# FetchContent需要额外引入
include(FetchContent)
# 声明fetch对象的属性
FetchContent_Declare(
  division-lib
  GIT_REPOSITORY    git@git.sdp.nd:cmk/division-lib.git
  GIT_TAG           master
  SOURCE_DIR        "${CMAKE_CURRENT_BINARY_DIR}/division"
)

# available
FetchContent_MakeAvailable(division-lib)
...
add_executable(main ${SRC_CORE})
...
# 添加头文件的搜索路径,当然也可以像上面那样修改division库的CMakeList.txt,这里就无需添加了
target_include_directories(main PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/division/src)

添加测试代码

#include <stdio.h>
#include "minus/minus.h"
#include "add.h"
#include "multiple.h"
#include "division.h"

int main(){
  printf("1+2=%d\n", add(1, 2));
  printf("2-1=%d\n", minus(2, 1));
  printf("2*2=%ld\n", multiple(2, 2));
  printf("4/2=%f\n", divide(4, 2));
  return 0;
}

在根目录中的build子目录中执行构建,并执行结

➜  build git:(master) ✗ ./main
1+2=3
2-1=1
2*2=4
4/2=2.000000