计算机 · 2021年8月13日 0

Cmake

概述

直接写makefile有时显得过于繁琐,而且维护跨平台的项目时显得比较麻烦,这时候就可以考虑使用cmake。
cmake的核心是targets(动态库、静态库、可执行程序或者所谓的custom target),cmake通过解析这些这些targets的依赖决定构建的顺序和生成构建每个target的动作(makefile中的target则更加抽象和随意,甚至可以有phony targets)。

简单实例

一个简单的CMakeLists.txt:

#规定一个适用的最低cmake版本,非必须
cmake_minimum_required(VERSION 3.6)
#为此CMakeLists所属项目取一个名字,非必须
project(demo)

构建步骤:
假设此CMakeLists.txt的路径为A/CMakeLists.txt,则可以在A目录下创建一个build目录,然后在A/build目录下执行:

cmake ..

则cmake会依据A/CMakeLists.txt生成相应的makefile文件在A/build目录下,只要在build目录下执行make就可以构建此CMakeLists.txt中添加的target(这个CMakeLists中没有写任何targets,所以就什么都不构建,其实作为一个demo,这个CMakeListst.txt甚至可以是空的。。。)。cmake生成的Makefile应该还有clean等目标,具体可以查看cmake生成的Makefile文件内容。当修改了A/CMakeListst.txt的内容时,可以再次执行cmake ..让cmake生成新的Makefile。

CMakeLists中的一些语法及相关概念

  • 普通变量(Normal Variable)
    和常见编程语言中的变量概念差不多,一个CMakeListst.txt就相当于一个作用域。在父CMakeLists.txt中定义的变量在子CMakeLists.txt中可见,且CMakeLists.txt可以定义一个同名的变量来覆盖此变量(不会影响父CMakeLists.txt中的变量定义)。
  • Cache Entry
    全局唯一的变量。

CMakeListst中的常用指令(Project Commands)

设置项目名字

# 有时会用此命令指明项目是C或者C++项目
project(<PROJECT-NAME> [LANGUAGES] [<language-name>...])
project(<PROJECT-NAME>
        [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
        [DESCRIPTION <project-description-string>]
        [LANGUAGES <language-name>...])

允许编译汇编代码

# 打开编译汇编程序的选项:enable_language(ASM)
enable_language(<lang> [OPTIONAL] )

添加库

# 添加要构建的静态库/动态库/MODULE
add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            source1 [source2 ...])

# 导入一个已经编译好了的库,一般会结合set_target_properties(<name> PROPERTIES IMPORTED_LOCATION <library_file_path>)使用,指明导入的库所对应的so或.a文件,且文件路径最好为绝对路径。
add_library(<name> <SHARED|STATIC|MODULE|OBJECT|UNKNOWN> IMPORTED
            [GLOBAL])

# 创建目标文件的引用,其他的library或者executable可以通过
# add_library(... $<TARGET_OBJECTS:objlib> ...)或者add_executable(... $<TARGET_OBJECTS:objlib> ...)
# 来引用objlib所代表的一系列目标文件(假设下面这条指令中的name取为objlib的话)
add_library(<name> OBJECT <src>...)

# 为library创建别名,不能为导入的库创建别名
add_library(<name> ALIAS <target>)

# interface library,没用过,但是先记在这里
add_library(<name> INTERFACE [IMPORTED [GLOBAL]])

注意事项:cmake中非导入的库(add_library添加的库)和可执行文件(add_executable添加的可执行程序)是全局可见且必须唯一的。全局可见指的是可以在父CMakeLists.txt中引用子CMakeLists.txt中创建的target(当然子CMakeLists.txt可以使用父CMakeLists.txt中的target)。导入的库(add_library且为IMPORTED的库)的可见性范围默认是当前CMakeLists.txt和子CMakeLists.txt,但是如果在导入时添加了GLOBAL修饰,那么这个导入的库也将变为全局可见。唯一指两个全局可见的target不能拥有相同的名字。

添加可执行程序

add_executable(<name> [WIN32] [MACOSX_BUNDLE]
               [EXCLUDE_FROM_ALL]
               source1 [source2 ...])

add_executable(<name> IMPORTED [GLOBAL])

add_executable(<name> ALIAS <target>)

添加源文件

最直接的添加源文件的方式当然是通过add_library,add_executable方法直接添加源文件。不过有时候可能需要在符合一定条件的情况下才添加某些源文件,这时候可以用

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

aux_source_directory可以用来将一个目录下的源文件一次性加入到指定变量中。然而这个命令存在一个问题,就是用cmake生成的具体构建文件(makefile之类的构建系统的文件)无法感知该目录中有新的源文件加入。

aux_source_directory(<dir> <variable>)

我在两个地方用到过set_source_files_properties这条指令,一个是没有通过enable_language允许编译汇编程序时可以用这个指令指示cmake按照编译C/C++文件的方式编译汇编程序(否则cmake会提示判断不了汇编程序的语言,因为cmake是通过文件名后缀来判断语言类型的,而汇编程序的后缀为.S/.s,不再C/C++程序的后缀列表里面);另一个是通过这个指令单独为某个源文件添加编译选项。

set_source_files_properties([file1 [file2 [...]]]
                            PROPERTIES prop1 value1
                            [prop2 value2 [...]])

添加头文件

include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

注意事项:此指令添加的用于搜寻头文件的目录对子CMakeLists.txt也是生效的。为了避免这种困扰,最好使用target_include_directories指令。

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

链接库

# Specify libraries or flags to use when linking any targets created later in the current directory or below 
link_libraries([item1 [item2 [...]]]
               [[debug|optimized|general] <item>] ...)
# 指明编译target时需链接的静态库/动态库
target_link_libraries(<target> ... <item>... ...)

注意事项:指定linker flag时应该使用此命令而不是用target_compile_options。比如:

target_link_libraries(${LIB_NAME} -Wl,-Bsymbolic)

指定链接时搜寻静态库/动态库的搜索路径

link_directories(directory1 directory2 ...)

定义宏

add_definitions(-DFOO -DBAR ...)

注意事项:此命令定义的宏对子CMakeLists.txt可见,甚至对调用此命令之前加入的子CMakeLists.txt都有效(?)。因此为避免困扰,最好是使用target_compile_definitions。

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

注意事项:像target_compile_definitions这类命令里的PUBLIC/INTERFACE/PRIVATE修饰的含义:PRIVATE只会将相应的设置应用于此target,而PUBLIC和INTERFACE则会将相应的设置应用于本target和链接了此target的target。

设置编译选项

add_compile_options(<option> ...)

与add_definitions类似,因此为了避免困扰,最好使用target_compile_definitions代替。

target_compile_options(<target> [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

添加一个子项目

add_subdirectory(source_dir [binary_dir]
                 [EXCLUDE_FROM_ALL])

注意事项:如果子项目不是在当前项目的子目录,必须手动指定该目录的构建目录(即所谓的binary_dir),为了方便,可以将该构建目录设置为${CMAKE_CURRENT_BINARY_DIR}/subproject_name

cmake中的变量设置及control flow

设置变量

# set normal variable
set(<variable> <value>... [PARENT_SCOPE])

# set cache entry
set(<variable> <value>... CACHE <type> <docstring> [FORCE])

# 设置环境变量
set(ENV{<variable>} <value>...)

条件语句

if(expression)
  # then section.
  COMMAND1(ARGS ...)
  COMMAND2(ARGS ...)
  #...
elseif(expression2)
  # elseif section.
  COMMAND1(ARGS ...)
  COMMAND2(ARGS ...)
  #...
else(expression)
  # else section.
  COMMAND1(ARGS ...)
  COMMAND2(ARGS ...)
  #...
endif(expression)

从文件、目录或者函数中返回

return()

字符串操作

####################SEARCH AND REPLACE#####################
#FIND
string(FIND <string> <substring> <output variable> [REVERSE])


#REPLACE
string(REPLACE <match_string>
       <replace_string> <output variable>
       <input> [<input>...])

####################REGULAR EXPRESSIONS#####################

#REGEX MATCH
string(REGEX MATCH <regular_expression>
       <output variable> <input> [<input>...])


#REGEX MATCHALL
string(REGEX MATCHALL <regular_expression>
       <output variable> <input> [<input>...])

#REGEX REPLACE
string(REGEX REPLACE <regular_expression>
       <replace_expression> <output variable>
       <input> [<input>...])


####################STRING MANIPULATION#####################

#APPEND
string(APPEND <string variable> [<input>...])

#PREPEND
string(PREPEND <string variable> [<input>...])

#CONCAT
string(CONCAT <output variable> [<input>...])

#TOLOWER
string(TOLOWER <string1> <output variable>)

#TOUPPER
string(TOUPPER <string1> <output variable>)

#LENGTH
string(LENGTH <string> <output variable>)

#SUBSTRING
string(SUBSTRING <string> <begin> <length> <output variable>)

#STRIP
string(STRIP <string> <output variable>)

#GENEX_STRIP
string(GENEX_STRIP <input string> <output variable>)

#Comparison
tring(COMPARE LESS <string1> <string2> <output variable>)
string(COMPARE GREATER <string1> <string2> <output variable>)
string(COMPARE EQUAL <string1> <string2> <output variable>)
string(COMPARE NOTEQUAL <string1> <string2> <output variable>)
string(COMPARE LESS_EQUAL <string1> <string2> <output variable>)
string(COMPARE GREATER_EQUAL <string1> <string2> <output variable>)

#HASHING
string(<HASH> <output variable> <input>)


###############################GENERATION#####################

#ASCII
string(ASCII <number> [<number> ...] <output variable>)

#CONFIGURE
string(CONFIGURE <string1> <output variable>
       [@ONLY] [ESCAPE_QUOTES])

#RANDOM
string(RANDOM [LENGTH <length>] [ALPHABET <alphabet>]
       [RANDOM_SEED <seed>] <output variable>)

#TIMESTAMP
string(TIMESTAMP <output variable> [<format string>] [UTC])


#UUID
string(UUID <output variable> NAMESPACE <namespace> NAME <name>
       TYPE <MD5|SHA1> [UPPER])

打印消息

message([<mode>] "message to display" ...)

设置选项

option(<option_variable> "help string describing option"
       [initial value])

注意事项:比较坑爹的是option是一个cache entry,而且option的一个规则是在任意一个CMakeLists.txt中设设置了某个option后,在后面的CMakeLists.txt中再尝试设置这个option的操作都会被cmake忽略掉。这就导致了在父CMakeLists.txt中设置选项A为ON,然后在不同的子CMakeLists.txt中分别设置选项A为ON和OFF的想法是用option实现不了的,想要实现这种操作需要用normal variable。适用于option的设置是那种真正全局同一的设置。

文件操作

在cmake中用file命令可以对文件进行读写甚至还可以从网上下载文件!不过我只用过下面的文件拷贝命令:

file(<COPY|INSTALL> <files>... DESTINATION <dir>
     [FILE_PERMISSIONS <permissions>...]
     [DIRECTORY_PERMISSIONS <permissions>...]
     [NO_SOURCE_PERMISSIONS] [USE_SOURCE_PERMISSIONS]
     [FILES_MATCHING]
     [[PATTERN <pattern> | REGEX <regex>]
      [EXCLUDE] [PERMISSIONS <permissions>...]] [...])

查找库

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

find_library (
          <VAR>
          name | NAMES name1 [name2 ...] [NAMES_PER_DIR]
          [HINTS path1 [path2 ... ENV var]]
          [PATHS path1 [path2 ... ENV var]]
          [PATH_SUFFIXES suffix1 [suffix2 ...]]
          [DOC "cache documentation string"]
          [NO_DEFAULT_PATH]
          [NO_CMAKE_PATH]
          [NO_CMAKE_ENVIRONMENT_PATH]
          [NO_SYSTEM_ENVIRONMENT_PATH]
          [NO_CMAKE_SYSTEM_PATH]
          [CMAKE_FIND_ROOT_PATH_BOTH |
           ONLY_CMAKE_FIND_ROOT_PATH |
           NO_CMAKE_FIND_ROOT_PATH]
         )

在Android Studio的向项目添加C/C++代码教程中使用过的命令。不知道这条命令存在的意义是什么,感觉要么直接用target的名字来引用库,要么用IMPORT库的方式来引用库就可以了,可能存在的意义在于不用指明库的具体路径,只需要指出库可能存在的文件目录,让写cmake的人可以偷点懒。

编写宏

macro(<name> [arg1 [arg2 [arg3 ...]]])
  COMMAND1(ARGS ...)
  COMMAND2(ARGS ...)
  ...
endmacro(<name>)

可以用${ARGV}引用参数列表,${ARGC}参数个数,${ARGV0},${ARGV1},${ARGV2}...引用传入的第1、2、3个参数。

编写函数

function(<name> [arg1 [arg2 [arg3 ...]]])
  COMMAND1(ARGS ...)
  COMMAND2(ARGS ...)
  ...
endfunction(<name>)

cmake中函数与宏的辨析
简言之,宏在引用传入的形式参数的值时使用的是字符串替换的方式;而函数才是真正的引用变量。

踩过的坑

1.改变CmakeLists.txt中的option,不会导致更新CmakeCache.txt中的option的值。

https://stackoverflow.com/a/35745766/5357784

https://stackoverflow.com/a/47325758/5357784

按照上面的回答,这个option只会在生成CmakeCache.txt时起作用,后续想要更改CMakeCache.txt中option的值,只有把CmakeCache.txt删掉重新生成(或者手动改CMakeCache.txt这个文件?)。

option的作用应该是说明有这样一个选项和提供初始值,后续想要变更这个选项可以通过上面的删除/修改CMakeCache.txt的方式或者在build时通过命令行参数指定option的值。