最近更新于 2022-04-18 10:41

在实际开发中,仅仅通过使用 gcc 命令对程序进行编译是非常低效的,原因主要有以下两点:

(1)程序往往是由多个源文件组成(某些程序项目可能成千上万个源码),源文件的个数越多,gcc 命令就会越长。此外,各种编译规则也会加大 gcc 命令的复杂度,所以在开发调试程序的过程中,通过输入 gcc 命令来编译是很麻烦的。

(2)在程序的整个开发过程中,调试的工作量往往会占据过半。在调试的过程中,每次调试可能只会修改部分源文件。而使用 gcc 命令编译会把那些没有被修改的源文件重新编译,这样会造成过多不必要的开销(有些工程项目完整编译一次可能就要花掉几个小时的时间)。

通过 cmake 来控制编译器则可以很好的解决,需要根据工程自己写一套编译的具体规则,例如,说明在哪里提取某个源码,在哪里调用某个库,在哪里引用某个头文件……规则写成以后,后面就可以通过执行 cmake 命令根据写好的规则来控制整个工程的构建,而不是每次编译都输入一个成千上万个字符长度的命令。构建的时候还会比较源码和生成目标文件的时间,判断是否修改了某些文件,只有修改的才重新编译,也解决了上面提到的问题。

文档

官方文档:https://cmake.org/documentation

中文参考资料:https://www.bookstack.cn/read/CMake-Cookbook/README.md

部分基础使用

编译所在系统判断

if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
    message("Linux")
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
    message("Windows")
else ()
    message("Unknow System!")
endif ()

编译器判断

if (${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU")
    message("GNU gcc/g++")
elseif (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")
    message("Clang")
elseif (${CMAKE_CXX_COMPILER_ID} STREQUAL "MSVC")
    message("MSVC")
else ()
    message("Unknow Compiler")
endif ()

GNU gcc/g++ 我自用的参数

注:如果是编译库,删除 -pie-no

sanitizer 调试参数

-fsanitize=address -fsanitize=leak -fsanitize=undefined 

C Release

set(CMAKE_C_FLAGS_RELEASE "-no-pie -O3")

C Debug

set(CMAKE_C_FLAGS_DEBUG "-std=c17 -no-pie -Wall -Werror=return-type -Werror=address -Werror=sequence-point -Werror=format-security -Wextra -pedantic -Wimplicit-fallthrough -Wsequence-point -Wswitch-unreachable -Wswitch-enum -Wstringop-truncation -Wbool-compare -Wtautological-compare -Wfloat-equal -Wshadow=global -Wpointer-arith -Wpointer-compare -Wcast-align -Wcast-qual -Wwrite-strings -Wdangling-else -Wlogical-op -Wconversion -g -O0 -DDEBUG")

C++ Release

set(CMAKE_CXX_FLAGS_RELEASE "-no-pie -O3")

C++ Debug

set(CMAKE_CXX_FLAGS_DEBUG "-std=c++17 -no-pie -Wall -Werror=return-type -Werror=non-virtual-dtor -Werror=address -Werror=sequence-point -Werror=format-security -Wextra -pedantic -Wimplicit-fallthrough -Wsequence-point -Wswitch-unreachable -Wswitch-enum -Wstringop-truncation -Wbool-compare -Wtautological-compare -Wfloat-equal -Wshadow=global -Wpointer-arith -Wpointer-compare -Wcast-align -Wcast-qual -Wwrite-strings -Wdangling-else -Wlogical-op -Wconversion -g -O0 -DDEBUG")

自动获取源码文件名

aux_source_directory(源码所在文件夹名 用于存放源码文件名的变量)

也可以使用 SET() 手动设置一个含有源码路径名的变量

将源码编译生成可执行文件

set(EXECUTABLE_OUTPUT_PATH 目标生成路径)
add_executable(要生成的可执行文件的名字 ${存放源码文件名的变量}})

设置项目名称

project(项目名称)

设置搜索头文件的路径

include_directories(头文件路径)

链接库文件到可执行文件中

target_link_libraries(可执行文件名 库名)

在一个大工程内,某个模块引用另外一个模块库,即设置的要使用的那个库的 project 名字

搜寻模块

使用示例见后面 示例-2 ,其中的模块使用的是 示例-1 中编译的静态库和动态库 – 更多使用可查阅官方文档

这里顺便记录一些我经常用到的第三方库寻找模块的写法

# SQLite3

find_package(SQLite3 REQUIRED)

target_link_libraries (可执行文件名 ${SQLite3_LIBRARIES})
# wiringPi

find_path(WIRINGPI_INCLUDE_DIRS NAMES wiringPi.h)
find_library(WIRINGPI_LIBRARIES NAMES wiringPi)

target_link_libraries(可执行文件名 ${WIRINGPI_LIBRARIES})
# OpenCV

find_package(OpenCV REQUIRED)

target_link_libraries(可执行文件名 ${OpenCV_LIBS})
# pthread

set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

target_link_libraries(可执行文件名 PRIVATE Threads::Threads)
# dlib

find_package(dlib REQUIRED)

target_link_libraries(可执行文件名 dlib::dlib)

生成库

set(LIBRARY_OUTPUT_PATH 目标生成路径)
add_library(静态库名 STATIC 源码路径名)
set(LIBRARY_OUTPUT_PATH 目标生成路径)
add_library(动态库名 SHARED 源码路径名)

设置工程中子模块路径

add_subdirectory(路径)

向终端输出信息

message(mode "信息内容")

mode 取值:

  • 留空  重要消息;
  • STATUS  非重要消息;
  • WARNING  警告, 但会继续执行;
  • AUTHOR_WARNING 警告 (dev), 会继续执行;
  • SEND_ERROR 错误, 继续执行,但是会跳过生成的步骤;
  • FATAL_ERROR 错误, 终止所有处理过程;

cmake –build . –target install 安装部署

简单使用就是,默认安装到 /usr/local 下

install(TARGETS 目标......)

比如后面 示例-1 的工程要安装,可以像下面这样写。demo 是可执行文件,默认放到 /usr/local/bin。sub 是动态库,默认放到 /usr/local/lib。工程中还有一个静态库 add,因为静态库是链接的时候就已经放入可执行文件了,也就没必要安装到系统中。

这样安装后执行 demo,可能会出现 demo: error while loading shared libraries: libsub.so: cannot open shared object file: No such file or directory 的错误,这是因为 demo 搜索不到 sub 库,/usr/local/lib 不是系统默认的搜索路径(或者放入系统的 /lib 库,不建议)。临时设置动态库搜索路径变量 LD_LIBRARY_PATH,执行 export LD_LIBRARY_PATH=/usr/local/lib:LD_LIBRARY_PATH。永久设置,向 /etc/ld.so.conf 中追加 /usr/local/lib 这个库路径,然后执行 sudo ldconfig 刷新。

install(TARGETS demo sub)

除上面提到的,其它部分预置变量

CMAKE_SOURCE_DIR 工程根目录路径
CMAKE_CURRENT_SOURCE_DIR CMakeLists.txt所在目录路径
CMAKE_SYSTEM 系统名字并带版本号
CMAKE_SYSTEM_PROCESSOR 处理器名称
CMAKE_CXX_COMPILER 编译器路径
$ENV{shell变量名} 获取shell变量的内容
CMAKE_SIZEOF_VOID_P  void*的sizeof大小,用于判断位宽

示例-1

工程结构

工程源码

使用

modules/static 下是加法运算的实现,将用作编译为静态库,编译后会放到 lib 下(libadd.a)

modules/shared 下是减法运算的实现,将用作编译为动态库,编译后会放到 lib 下(libsub.so)

samples 下是调用库进行使用,main() 所在

示例-2

工程结构

工程源码

使用

作者 IYATT-yx