CMake搭建项目工程(1)-C/C++编译及CMake那些事

为了将C/C++代码转换为可以在硬件上运行的程序,需要经过编译和链接。编译是将高级语言所写的源程序翻译成等价的机器语言或汇编语言的目标程序(.o文件),链接是把目标文件及相关库文件(动态链接库.so或静态链接库.a)进行组织,生成可执行代码的过程。


image.png

动态链接库与静态链接库

库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。 windows上对应的是.lib .dll,linux上对应的是.a .so。

静态库

静态库是在链接阶段将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。静态库对函数库的链接是放在代码编译的静态链接时期完成的,程序在运行时不再额外需要静态库,移植方便。在程序发布后,如果静态库发生变化,需要对使用它的应用程序重新编译及发布,因此部署灵活性及可扩展性较差。静态库在内存中存在多份,存在浪费空间的问题。

动态库

动态库是在程序运行时被载入,解决了静态库对程序发布带来的麻烦。用户只需要更新动态库即可,增量更新。动态库在内存中只存在一份拷贝,避免了静态库空间浪费问题。因为动态链接库在程序运行时才被载入,所以在发布时,需要与应用程序一起发布,这一点与静态库也有区别。
静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

动态库特点总结:

l 动态库把对一些库函数的链接载入推迟到程序运行的时期。

l 可以实现进程之间的资源共享。(因此动态库也称为共享库)

l 将一些程序升级变得简单。
静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)

CMake

CMake是一个跨平台的编译工具,可以用简单的语句来描述所有平台的编译过程。CMake并不直接构建出最终的软件,而是输出各种各样的makefile或者project文件,而用户可以使用这些生成的文件进行编译程序。CMake能够管理大型项目,相比直接书写makefile,CMake的编写更加高效并且消除了平台的差异性。以linux平台使用CMake编译为例:

  1. 编写 CMake 配置文件 CMakeLists.txt 。在项目中CMake配置文件一般为树形结构,项目总的CMake配置文件调用各模块的CMake配置文件。
  2. 执行命令 cmake PATH生成 Makefile。其中, PATH 是 CMakeLists.txt 所在的目录。(见下面的in-source build 和 out-of-source build)
  3. 使用 make 命令进行编译。

in-source build VS out-of-source build

in-source build 是指在 CMakeLists.txt所在的文件夹直接执行cmake
out-of-source build是指在非CMakeLists.txt目录执行cmake
两者的重要差异在于产生的中间目标文件(.obj)和可执行程序的位置,in-source build是这些文件和代码混杂在一起,而out-of-source build是在执行cmake的目录集中放置,在项目实际开发中,一般采用out-of-source build方式。而这两者的差异也在CMake的常量上有所体现。以CMAKE_BINARY_DIR(二进制目录)和CMAKE_SOURCE_DIR(代码目录)为例:对于in-source build,由于二进制中间文件和代码目录放在一起,所以这两个变量的取值是相同的;对于out-of-source build,CMAKE_BINARY_DIR(二进制目录)自然是执行cmake的目录,即二进制中间目标文件存放位置的顶级目录,而CMAKE_SOURCE_DIR是CMakeLists.txt所在的位置。CMake中其余的*_BINARY_DIR和 *_SOURCE_DIR变量的区别与示例完全一致。

CMake常用常量

name implication
CMAKE_BINARY_DIR if you are building in-source, this is the same as CMAKE_SOURCE_DIR, otherwise this is the top level directory of your build tree
CMAKE_CURRENT_BINARY_DIR if you are building in-source, this is the same as CMAKE_CURRENT_SOURCE_DIR, otherwise this is the directory where the compiled or generated files from the current CMakeLists.txt will go to.
CMAKE_CURRENT_SOURCE_DIR this is the directory where the currently processed CMakeLists.txt is located in
CMAKE_MODULE_PATH tell CMake to search first in directories listed in CMAKE_MODULE_PATH when you use FIND_PACKAGE() or INCLUDE() SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/MyCMakeScripts) FIND_PACKAGE(HelloWorld)
CMAKE_SOURCE_DIR this is the directory which contains the top-level CMakeLists.txt, i.e. the top level source directory
PROJECT_NAME the name of the project set by PROJECT() command.
CMAKE_PROJECT_NAME the name of the first project set by the PROJECT() command, i.e. the top level project.
PROJECT_BINARY_DIR contains the full path to the top level directory of your build tree
PROJECT_SOURCE_DIR contains the full path to the root of your project source directory, i.e. to the nearest directory where CMakeLists.txt contains the PROJECT() command

CMake常用命令

基本语法
COMMAND(args...),多个参数用空白符分隔

cmake_minimum_required(VERSION x.y.z)

作用:设置工程所需要的最低CMake版本。

aux_source_directory(<dir> <variable>)

作用:查找dir路径下的所有源文件,保存到variable变量中.
注意:

  1. aux_source_directory 不会递归包含子目录,仅包含指定的dir目录
  2. 在路径下增加源文件后,需要手动重新运行cmake,原因是在被搜索的路径下添加源文件,不需要修改CMakeLists脚本,构建系统不能察觉到新加的文件,如不手动运行cmake,新添加的文件就不会被编译。
add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] source1 source2 ... sourceN)

作用:使用指定的源文件source1 source2 ... sourceN向工程中添加一个名为<name>库。STATIC, SHARED, MODULE参数来指定要创建的库的类型, STATIC对应的静态库(.a),SHARED对应共享动态库(.so)

add_library(<name> [STATIC | SHARED | MODULE | UNKNOWN] IMPORTED)

作用:导入了一个已存在的<name>库文件,导入库一般配合set_target_properties使用,这个命令用来指定导入库的路径,比如:要导入一个位于 libs/src/arm64-v8a/libtest.so的动态库到工程,并且calc 库依赖与sum库,则:

add_library(sum SHARED IMPORTED)
set_target_properties(  sum #指定目标库名称
                        PROPERTIES IMPORTED_LOCATION #指明要设置的参数
                        libs/src/${ANDROID_ABI}/libsum.so #设定导入库的路径)
target_link_libraries(calc sum)#将库sum链接到calc库
//注意:动态链接库与静态链接库的区别,calc库需调用sum库,因此必须一起发布
set(<variable> <value>)

作用:将变量<variable>的值设置为<value>。如果没有指定<value>,那么这个变量就会被撤销而不是被设置。
它可以用来设置全局变量,也可以用来设置自变量。使用时采用${variable}进行变量的引用。在 IF 等语句中,是直接使用变量名而不通过${variable}取值

# 设置库文件的输出路径
set(LIBRARY_OUTPUT_PATH libs/src/${ANDROID_ABI})

# 设置C++编译参数
set(CMAKE_CXX_FLAGS "-Wall std=c++11")

# 设置源文件集合(SOURCE_FILES是自变量)
set(SOURCE_FILES main.cpp sum.cpp ...)
include_directories([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)

作用:设置头文件位置,dir为位置信息,可为相对或绝对路径,要特别注意其与target_include_directories的区别。

target_include_directories(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...])

作用:设置名为target的头文件路径。它与include_directories的区别在于:
include_directory 影响目录范围为该CMakeList.txt中的所有目标,以及调用后添加的所有子目录中的目标,都将把路径添加到它们的include路径中。而target_include_directory具有更精确的目标范围,它将依赖目录添加到目标target的包含路径中。
如果所有目标都使用所涉及的include目录,则需要使用include_directories。如果路径是特定于某个目标的,或者希望更好地控制路径的可见性,则需要target_include_directory。

add_executable(<name> source1 source2 ... sourceN)

作用:使用给定的源文件source1 source2 ... sourceN,为工程添加名为name可执行文件

target_link_libraries(<target>lib1 lib2 lib3)

作用:将若干库lib1 lib2 lib3链接到目标文件target上。CMake提供了link_libraries也可以链接库,但是官方更加建议使用target_link_libraries,因为它的控制可以更加精确。
注意:

  1. 链接的顺序要符合gcc的链接顺序,被链接的库放在依赖他的库的后面,如calc库依赖于sum库,则应使用target_link_libraries(test calc sum),其中calc 与sum的前后顺序不能颠倒。
  2. 默认时,库之间的依赖性是可传递的。当这个目标被链接到其他目标上时,那么链接到这个目标上的库也会出现在其他目标的链接依赖上。
  3. 目标文件target必须在target_link_libraries调用之前由add_executable或add_library创建。
add_subdirectory(source_dir)

作用:为构建添加一个子路径。source_dir选项指定了CMakeLists.txt源文件和代码文件的位置。source_dir可以为相对路径也可以为绝对路径,当为相对路径时,为相对当前CMakeList.txt所在目录的相对路径。

file

作用:文件操作命令
注意:

  1. GLOB 与GLOB_RECURSE ,CMake官方不建议使用,因为增加和删除源文件,构建出的makefile不会感知到,此时需要重新使用cmake命令产生构建系统文件(makefile)。
  2. GLOB 与GLOB_RECURSE中的匹配表达式与正则表达式类似,只不过更加简单。。Cmake官方文档的几个例子:
    • *.cxx - 匹配所有扩展名为cxx的文件。
    • *.vt? - 匹配所有扩展名是vta,...,vtz的文件。
    • f[3-5].txt - 匹配文件f3.txt, f4.txt, f5.txt。
// 向文件filename中写入,会覆盖文件原有内容
file(WRITE filename "message to write"... )
// 向文件filename中追加写入
file(APPEND filename "message to write"... )
//从文件filename中读取内容,并存储到variable变量中
//如果指定了numBytes和offset,则从offset处开始最多读numBytes个字节
//如果指定了HEX参数,则内容会以十六进制形式存储在var变量中
file(READ filename variable [LIMIT numBytes] [OFFSET offset] [HEX])
//为所有匹配查询表达式的文件生成一个文件list,并将该list存储进变量variable里。
//如果为一个表达式指定了RELATIVE标志,返回的结果将会是相对于给定路径的相对路径
file(GLOB variable [RELATIVE path] [globbing expressions]...)
//GLOB_RECURSE与GLOB的差异在于,GLOB_RECURSE会递归查询
file(GLOB_RECURSE variable [RELATIVE path]
       [FOLLOW_SYMLINKS] [globbing expressions]...)
//重命名文件
file(RENAME <oldname> <newname>)
//删除文件,类似于rm
file(REMOVE [file1 ...])
//递归的执行删除文件命令, 类似于rm -r
file(REMOVE_RECURSE [file1 ...])
//创建目录
file(MAKE_DIRECTORY [directory1 directory2 ...])
//确定从direcroty参数到指定文件file的相对路径,并保存在variable中
file(RELATIVE_PATH variable directory file)
//根据指定的url下载文件,timeout是超时时间,status是下载的状态,log是下载日志
//sum是所下载文件预期的MD5值,SHOW_PROGRESS是进度信息打印
file(DOWNLOAD url file [TIMEOUT timeout] [STATUS status] [LOG log]
       [EXPECTED_MD5 sum] [SHOW_PROGRESS])
add_dependencies(target-name depend-target1 depend-target2 ...)

作用:为顶层目标target-name引入依赖depend-target1,depend-target2。顶层目标target-name是由命令ADD_EXECUTABLE,ADD_LIBRARY,或者ADD_CUSTOM_TARGET产生的目标。该命令可以在编译上层“target-name ”时,自动检查下层依赖库是否已经生成。没有的话先编译下层依赖库(depend-target1,depend-target2),然后再编译上层target,最后link depend target。

define_property(<GLOBAL | DIRECTORY | TARGET | SOURCE | TEST | VARIABLE | CACHED_VARIABLE> PROPERTY <name> [INHERITED] BRIEF_DOCS <brief-doc> [docs...] FULL_DOCS <full-doc> [docs...])

作用:定义并描述自定义属性.
 在一个域(scope)中定义一个可以用set_property和get_property命令访问的属性。这个命令一般与get_property命令结合使用,该命令定义的属性可以通过get_property获取。第一个参数确定了这个属性可以使用的范围。它必须是:

  • GLOBAL = 与全局命名空间相关联
  • DIRECTORY = 与某一个目录相关联
  • TARGET = 与一个目标相关联
  • SOURCE = 与一个源文件相关联
  • TEST = 与一个以add_test命名的测试相关联
  • VARIABLE = 描述一个CMake语言变量
  • CACHED_VARIABLE = 描述一个CMake语言缓存变量

注意:
与set_property和get_property不同,不需要给出实际的作用域,只需要给出作用域类型。PROPERTY选项必须设置,它为定义的属性名称。如果指定了INHERITED选项,那么如果get_property命令所请求的属性在该作用域中未设置,它会沿着链条向更高的作用域去搜索。DIRECTORY域向上是GLOBAL。TARGET,SOURCE和TEST向上是DIRECTORY。BRIEF_DOCS和FULL_DOCS选项后面的参数是和属性相关联的字符串,分别为变量的简单描述和完整描述。在使用get_property命令时,对应的选项可以获取这些描述信息。

find_path(<VAR> name [path1 path2 ...])

作用:在path1、path2下搜索名称为name的文件,并将name的文件路径保存在VAR。

//在/usr/include/demo/  和  /usr/local/demo/include/路径下搜索demo.h
//并将文件路径保存在DEMOLIB_INCLUDE_DIR中
find_path(DEMOLIB_INCLUDE_DIR demo.h /usr/include/demo/
        /usr/local/demo/include/)
find_file(<VAR> name [path1 path2 ...])

作用:在path1、path2下搜索名称为name的文件,并将name的文件完整路径保存在VAR。
区别:find_path与find_file的区别在于,find_file的路径信息里包含文件本身,而find_path不带。

find_library(<VAR> name [path1 path2 ...])

作用:在path1、path2下搜索名称为name的库文件,并将name的库文件完整路径保存在VAR。(包含库文件名)

find_package(<package> [version] [EXACT] [QUIET[[REQUIRED|COMPONENTS] [components...]] [NO_POLICY_SCOPE])

作用:该命令用于在CMAKE_MODULE_PATH制定的路径下搜索Find<package>.cmake的文件,如果查找到了该文件,它会被CMake读取并并加载外来其设置。该命令会设置<package>_FOUND变量,用来指示要找的包是否被找到了。如果这个包被找到了,与它相关的信息可以通过包自身记载的变量中得到。QUIET选项将会禁掉包没有被发现时的警告信息。REQUIRED选项表示如果报没有找到的话,cmake的过程会终止,并输出警告信息。[version]参数需要一个版本号,它是正在查找的包应该兼容的版本号(格式是major[.minor[.patch[.tweak]]])。EXACT选项要求该版本号必须精确匹配。

CMake控制命令、安装及模块将在下一篇阐述。

WalkeR_ZG

推荐阅读更多精彩内容

  • 注:首发地址 1. 前言 当在做 Android NDK 开发时,如果不熟悉用 CMake 来构建,读不懂 CMa...
    cfanr阅读 10,361评论 1 37
  • CMake学习 本篇分享一下有关CMake的一些学习心得以及相关使用。 本文目录如下: [1、CMake介绍] [...
    AlphaGL阅读 7,855评论 11 76
  • 搬运自本人 CSDN 博客:https://blog.csdn.net/ajianyingxiaoqinghan/...
    琦小虾阅读 5,849评论 0 8
  • CMake 全称“cross platform make”,是开源、跨平台的自动化构建系统。CMake 由 Kit...
    神齐阅读 952评论 0 1
  • CMake学习 参考自《Cmake Practice --Cjacker》 基本语法规则 变量的引用 变量使用${...
    techping阅读 910评论 0 6