CMake入门
04 Jan 2021CMake是个一个开源的跨平台自动化建构系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录、多个应用程序与多个库。 它用配置文件控制建构过程(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_minimum_required
- project
- add_executable
这些方法的定义都可以在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_
- CFLAGS
- CXXFLAGS
- CMAKE_CUDA_FLAGS
- CMAKE_Fortran_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目录里执行构建,并执行构建结果
- 有的文档里可能会用其他的函数引入动态库。我们这里使用的库是3.x引入的官方推荐的方式
- 添加头文件查找目录可以用其他方式代替: 在minus目录下的CMakeLists.txt里增加头文件的定义
# 这里INTERFACE的意思是当前这个动态库不需要使用到,但是使用这个动态库的需要 target_include_directories(minus INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} )
然后根目录的CMakeLists.txt里的
target_include_directories
声明的${CMAKE_CURRENT_SOURCE_DIR}/src/minus
就可以不要了。
添加预编译动态库
下一步我们将在外部先编译一个外部库,然后引入这个外部库。假设我们已经编译好这个外部库,并将头文件和动态库文件放到根目录下的libs
和include
├── 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仓库的方式存在的,使用方式大概有几种
- 最简单的使用方式是将源码下载到当前目录下,并用
add_subdirectory
的方式进行依赖。 - 如果是git仓库可以使用git submodule的方式将第三方仓库加入到源码管理中
- 使用
ExternalProject
相关API进行管理 - 使用
FetchContent
相关的API进行管理
第一种方式更新源码比较麻烦,无法进行快速切换; 第二种方式使用过程中感觉比较不好不好使用。多层级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