📦 归档笔记 — 原创建于 WizNote,仅作归档展示;观点以当年为准,非最新。

Cmake的使用和CMakeLists.txt的编写

创建时间2020-12-05最后修改2020-12-06原位置/程序员成长之旅/C++/字数4122
目录:程序员成长之旅/C++

PROJECT 指令的语法是: PROJECT(projectname [CXX] [C] [Java])

1

1 PROJECT ( projectname [ CXX ] [ C ] [ Java ]) 你可以用这个指令定义工程名称,并可指定工程支持的语言,支持的语言列表是可以忽略的, 默认情况表示支持所有语言。这个指令隐式的定义了两个 cmake 变量 : <projectname> _BINARY_DIR 以及 <projectname> _SOURCE_DIR ,这里就是 HELLO_BINARY_DIR 和 HELLO_SOURCE_DIR( 所以 CMakeLists.txt 中两个 MESSAGE 指令可以直接使用了这两个变量 ) ,因为采用的是内部编译,两个变量目前指的都是工程所 在路径 /backup/cmake/t1 ,后面我们会讲到外部编译,两者所指代的内容会有所不同。 同时 cmake 系统也帮助我们预定义了 PROJECT_BINARY_DIR 和 PROJECT_SOURCE_DIR 变量,他们的值分别跟 HELLO_BINARY_DIR 与 HELLO_SOURCE_DIR 一致。 为了统一起见,建议以后直接使用 PROJECT_BINARY_DIR , PROJECT_SOURCE_DIR ,即 使修改了工程名称,也不会影响这两个变量。如果使用了 <projectname> _SOURCE_DIR ,修改工程名称后,需要同时修改这些变量。

SET 指令的语法是:

SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])

1

1 SET ( VAR [ VALUE ] [ CACHE

TYPE

DOCSTRING [ FORCE ]])

现阶段,你只需要了解 SET 指令可以用来显式的定义变量即可。 比如我们用到的是 SET(SRC_LIST main.c) ,如果有多个源文件,也可以定义成: SET(SRC_LIST main.c t1.c t2.c) 。

MESSAGE 指令的语法是: MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message to display"...)

1

1 MESSAGE ([ SEND_ERROR

|

STATUS

|

FATAL_ERROR ] "message to display" ...)

这个指令用于向终端输出用户定义的信息,包含了三种类型 : SEND_ERROR ,产生错误,生成过程被跳过。 SATUS ,输出前缀为 — 的信息。 FATAL_ERROR ,立即终止所有 cmake 过程 . 我们在这里使用的是 STATUS 信息输出,演示了由 PROJECT 指令定义的两个隐式变量 HELLO_BINARY_DIR 和 HELLO_SOURCE_DIR 。

ADD_EXECUTABLE 指令的语法是: ADD_EXECUTABLE(hello ${SRC_LIST})

1

1 ADD_EXECUTABLE ( hello

$ { SRC_LIST })

定义了这个工程会生成一个文件名为 hello 的可执行文件,相关的源文件是 SRC_LIST 中 定义的源文件列表, 本例中你也可以直接写成 ADD_EXECUTABLE(hello main.c) 。 在本例我们使用了 ${} 来引用变量,这是 cmake 的变量应用方式,但是,有一些例外,比 如在 IF 控制语句,变量是直接使用变量名引用,而不需要 ${} 。如果使用了 ${} 去应用变 量,其实 IF 会去判断名为 ${} 所代表的值的变量,那当然是不存在的了。

基本语法规则 前面提到过, cmake 其实仍然要使用 ”cmake 语言和语法 ” 去构建,上面的内容就是所谓的 ”cmake 语言和语法 ” ,最简单的语法规则是: 1 ,变量使用 ${} 方式取值,但是在 IF 控制语句中是直接使用变量名 2 ,指令 ( 参数 1 参数 2...) 参数使用括弧括起,参数之间使用空格或分号分开。 以上面的 ADD_EXECUTABLE 指令为例,如果存在另外一个 func.c 源文件,就要写成: ADD_EXECUTABLE(hello main.c func.c) 或者 ADD_EXECUTABLE(hello main.c;func.c) 3 ,指令是大小写无关的,参数和变量是大小写相关的。但,推荐你全部使用大写指令。 上面的 MESSAGE 指令我们已经用到了这条规则: MESSAGE(STATUS “This is BINARY dir” ${HELLO_BINARY_DIR}) 也可以写成: MESSAGE(STATUS “This is BINARY dir ${HELLO_BINARY_DIR}”) 这里需要特别解释的是作为工程名的 HELLO 和生成的可执行文件 hello 是没有任何关系的。 hello 定义了可执行文件的文件名,你完全可以写成: ADD_EXECUTABLE(t1 main.c) 编译后会生成一个 t1 可执行文件。

关于语法的疑惑 cmake 的语法还是比较灵活而且考虑到各种情况,比如 SET(SRC_LIST main.c) 也可以写成 SET(SRC_LIST “main.c”) 是没有区别的,但是假设一个源文件的文件名是 fu nc.c( 文件名中间包含了空格 ) 。 这时候就必须使用双引号,如果写成了 SET(SRC_LIST fu nc.c) ,就会出现错误,提示 你找不到 fu 文件和 nc.c 文件。这种情况,就必须写成 : SET(SRC_LIST “fu nc.c”) 此外,你可以可以忽略掉 source 列表中的源文件后缀,比如可以写成 ADD_EXECUTABLE(t1 main) , cmake 会自动的在本目录查找 main.c 或者 main.cpp 等,当然,最好不要偷这个懒,以免这个目录确实存在一个 main.c 一个 main. 同时参数也可以使用分号来进行分割。 下面的例子也是合法的: ADD_EXECUTABLE(t1 main.c t1.c) 可以写成 ADD_EXECUTABLE(t1 main.c;t1.c). 我们只需要在编写 CMakeLists.txt 时注意形成统一的风格即可。

清理工程: 跟经典的 autotools 系列工具一样,运行 : make clean

1

1 make

clean

即可对构建结果进行清理。

内部构建与 外部构建 : 上面的例子展示的是 “ 内部构建 ” ,相信看到生成的临时文件比您的代码文件还要多的时候, 估计这辈子你都不希望再使用内部构建 :-D 举个简单的例子来说明外部构建,以编译 wxGTK 动态库和静态库为例,在 Everest 中打包 方式是这样的: 解开 wxGTK 后。 在其中建立 static 和 shared 目录。 进入 static 目录,运行 ../configure –enable-static;make 会在 static 目录生 成 wxGTK 的静态库。 进入 shared 目录,运行 ../configure –enable-shared;make 就会在 shared 目录 生成动态库。 这就是外部编译的一个简单例子。 对于 cmake ,内部编译上面已经演示过了,它生成了一些无法自动删除的中间文件,所以, 引出了我们对外部编译的探讨,外部编译的过程如下: 1 ,首先,请清除 t1 目录中除 main.c CmakeLists.txt 之外的所有中间文件,最关键 的是 CMakeCache.txt 。 2 ,在 t1 目录中 建立 build 目录 ,当然你也可以在任何地方建立 build 目录,不一定必 须在工程目录中。 3 , 进入 build 目录,运行 cmake .. ( 注意 ,.. 代表父目录,因为父目录存在我们需要的 CMakeLists.txt ,如果你在其他地方建立了 build 目录,需要运行 cmake < 工程的全 路径 >) ,查看一下 build 目录,就会发现了生成了编译需要的 Makefile 以及其他的中间 文件 . 4 ,运行 make 构建工程,就会在当前目录 (build 目录 ) 中获得目标文件 hello 。 上述过程就是所谓的 out-of-source 外部编译,一个最大的好处是,对于原有的工程没 有任何影响, 所有动作全部发生在编译目录 。通过这一点,也足以说服我们全部采用外部编 译方式构建工程。 这里需要特别注意的是: 通过外部编译进行工程构建, HELLO_SOURCE_DIR 仍然指代工程路径 , 即 /backup/cmake/t1 而 HELLO_BINARY_DIR 则指代编译路径 , 即 /backup/cmake/t1/build

ADD_SUBDIRECTORY 指令 ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

1

1 ADD_SUBDIRECTORY ( source_dir [ binary_dir ] [ EXCLUDE_FROM_ALL ]) 这个指令用于向当前工程添加存放源文件的子目录, 并可以指定中间二进制和目标二进制存 放的位置 。 EXCLUDE_FROM_ALL 参数的含义是将这个目录从编译过程中排除,比如,工程 的 example ,可能就需要工程构建完成后,再进入 example 目录单独进行构建 ( 当然,你 也可以通过定义依赖来解决此类问题 ) 。 上面的例子定义了将 src 子目录加入工程,并指定编译输出 ( 包含编译中间结果 ) 路径为 bin 目录。如果不进行 bin 目录的指定,那么编译结果 ( 包括中间结果 ) 都将存放在 build/src 目录 ( 这个目录跟原有的 src 目录对应 ) , 指定 bin 目录后,相当于在编译时 将 src 重命名为 bin ,所有的中间结果和目标二进制都将存放在 bin 目录。

换个地方保存目标二进制 不论是 SUBDIRS 还是 ADD_SUBDIRECTORY 指令 ( 不论是否指定编译输出目录 ) ,我们都可 以通过 SET 指令重新定义 EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH 变量 来指定最终的目标二进制的位置 ( 指最终生成的 hello 或者最终的共享库 ,不包含编译生成 的中间文件 ) SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

2

1 SET ( EXECUTABLE_OUTPUT_PATH

$ { PROJECT_BINARY_DIR } / bin ) 2 SET ( LIBRARY_OUTPUT_PATH

$ { PROJECT_BINARY_DIR } / lib )

在第一节我们提到了 <projectname> _BINARY_DIR 和 PROJECT_BINARY_DIR 变量, 他 们指的编译发生的当前目录 ,如果是内部编译,就相当于 PROJECT_SOURCE_DIR 也就是 工程代码所在目录,如果是 外部编译 ,指的是外部编译所在目录, 也就是本例中的 build 目录。 所以,上面两个指令分别定义了: 可执行二进制的输出路径为 build/bin 和库的输出路径为 build/lib. 本节我们没有提到共享库和静态库的构建,所以,你可以不考虑第二条指令。 问题是,我应该把这两条指令写在工程的 CMakeLists.txt 还是 src 目录下的 CMakeLists.txt , 把握一个简单的原则,在哪里 ADD_EXECUTABLE 或 ADD_LIBRARY , 如果需要改变目标存放路径,就在哪里加入上述的定义。

如何安装 安装的需要有两种,一种是从代码编译后直接 make install 安装,一种是打包时的指定 目录安装。 所以,即使最简单的手工编写的 Makefile ,看起来也是这个样子的: DESTDIR= install: mkdir -p $(DESTDIR)/usr/bin install -m 755 hello $(DESTDIR)/usr/bin 你可以通过 : make install 将 hello 直接安装到 /usr/bin 目录,也可以通过 make install DESTDIR=/tmp/test 将他安装在 /tmp/test/usr/bin 目录,打包时这个方式经常被使用。 稍微复杂一点的是还需要定义 PREFIX ,一般 autotools 工程,会运行这样的指令 : ./configure –prefix=/usr 或者 ./configure --prefix=/usr/local 来指定 PREFIX 比如上面的 Makefile 就可以改写成 : DESTDIR= PREFIX=/usr install: mkdir -p $(DESTDIR)/$(PREFIX)/bin install -m 755 hello $(DESTDIR)/$(PREFIX)/bin 那么我们的 HelloWorld 应该怎么进行安装呢? 这里需要引入一个新的 cmake 指令 INSTALL 和一个非常有用的变量 CMAKE_INSTALL_PREFIX 。 CMAKE_INSTALL_PREFIX 变量 类似于 configure 脚本的 –prefix ,常见的使用方法看 起来是这个样子: cmake -DCMAKE_INSTALL_PREFIX=/usr .

1

1 cmake

DCMAKE_INSTALL_PREFIX =/ usr .

INSTALL 指令用于定义安装规则,安装的内容可以包括目标二进制、动态库、静态库以及 文件、目录、脚本等。 INSTALL 指令包含了各种安装类型,我们需要一个个分开解释: 目标文件的安装: INSTALL(TARGETS targets... [[ARCHIVE|LIBRARY|RUNTIME] [DESTINATION <dir> ] [PERMISSIONS permissions...] [CONFIGURATIONS] [Debug|Release|...]] [COMPONENT <component> ] [OPTIONAL] ] [...])

9

1

INSTALL ( TARGETS

targets ... 2 [[ ARCHIVE | LIBRARY | RUNTIME ] 3

[ DESTINATION

< dir

] 4

[ PERMISSIONS

permissions ...] 5

[ CONFIGURATIONS ] 6

[ Debug | Release | ...]] 7 [ COMPONENT

< component

] 8 [ OPTIONAL ] 9 ] [...])

参数中的 TARGETS 后面跟的就是我们通过 ADD_EXECUTABLE 或者 ADD_LIBRARY 定义的 目标文件 ,可能是可执行二进制、动态库、静态库。 目标类型也就相对应的有三种, ARCHIVE 特指静态库 , LIBRARY 特指动态库 , RUNTIME 特指可执行目标二进制 。 DESTINATION 定义了安装的路径 ,如果路径以 / 开头,那么指的是绝对路径,这时候 CMAKE_INSTALL_PREFIX 其实就无效了。如果你希望使用 CMAKE_INSTALL_PREFIX 来 定义安装路径,就要写成相对路径,即不要以 / 开头,那么安装后的路径就是 ${CMAKE_INSTALL_PREFIX}/ <DESTINATION 定义的路径>

1

1 $ { CMAKE_INSTALL_PREFIX } /< DESTINATION

定义的路径

举个简单的例子:

INSTALL(TARGETS myrun mylib mystaticlib RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION libstatic )

5

1 INSTALL ( TARGETS

myrun

mylib

mystaticlib 2

RUNTIME

DESTINATION

bin 3

LIBRARY

DESTINATION

lib 4

ARCHIVE

DESTINATION

libstatic 5 )

上面的例子会将: 可执行二进制 myrun 安装到 ${CMAKE_INSTALL_PREFIX}/bin 目录 动态库 libmylib 安装到 ${CMAKE_INSTALL_PREFIX}/lib 目录 静态库 libmystaticlib 安装到 ${CMAKE_INSTALL_PREFIX}/libstatic 目录 特别注意的是你不需要关心 TARGETS 具体生成的路径,只需要写上 TARGETS 名称就可以 了。

普通文件的安装: INSTALL(FILES files... DESTINATION <dir> [PERMISSIONS permissions...] [CONFIGURATIONS [Debug|Release|...]] [COMPONENT <component> ] [RENAME <name> ] [OPTIONAL])

5

1 INSTALL ( FILES

files ... DESTINATION

< dir

2 [ PERMISSIONS

permissions ...] 3 [ CONFIGURATIONS [ Debug | Release | ...]] 4 [ COMPONENT

< component

] 5 [ RENAME

< name

] [ OPTIONAL ]) (头文件使用该种方式安装) 可用于安装一般文件,并可以指定访问权限,文件名是此指令所在路径下的相对路径。 如果 默认不定义权限 PERMISSIONS ,安装后的权限为: OWNER_WRITE, OWNER_READ, GROUP_READ, 和 WORLD_READ ,即 644 权限。

非目标文件的可执行程序安装 ( 比如脚本之类 ) : INSTALL(PROGRAMS files... DESTINATION <dir> [PERMISSIONS permissions...] [CONFIGURATIONS [Debug|Release|...]] [COMPONENT <component> ] [RENAME <name> ] [OPTIONAL])

5

1 INSTALL ( PROGRAMS

files ... DESTINATION

< dir

2 [ PERMISSIONS

permissions ...] 3 [ CONFIGURATIONS [ Debug | Release | ...]] 4 [ COMPONENT

< component

] 5 [ RENAME

< name

] [ OPTIONAL ]) 跟上面的 FILES 指令使用方法一样, 唯一的不同是安装后权限为 : OWNER_EXECUTE, GROUP_EXECUTE, 和 WORLD_EXECUTE ,即 755 权限

目录的安装:

INSTALL(DIRECTORY dirs... DESTINATION <dir> [FILE_PERMISSIONS permissions...] [DIRECTORY_PERMISSIONS permissions...] [USE_SOURCE_PERMISSIONS] [CONFIGURATIONS [Debug|Release|...]] [COMPONENT <component> ] [[PATTERN <pattern> | REGEX <regex> ] [EXCLUDE] [PERMISSIONS permissions...]] [...])

8

1 INSTALL ( DIRECTORY

dirs ... DESTINATION

< dir

2 [ FILE_PERMISSIONS

permissions ...] 3 [ DIRECTORY_PERMISSIONS

permissions ...] 4 [ USE_SOURCE_PERMISSIONS ] 5 [ CONFIGURATIONS [ Debug | Release | ...]] 6 [ COMPONENT

< component

] 7 [[ PATTERN

< pattern

|

REGEX

< regex

] 8 [ EXCLUDE ] [ PERMISSIONS

permissions ...]] [...]) 这里主要介绍其中的 DIRECTORY 、 PATTERN 以及 PERMISSIONS 参数。 DIRECTORY 后面连接的是所在 Source 目录的相对路径 ,但务必注意: abc 和 abc/ 有很大的区别。 如果目录名不以 / 结尾,那么这个目录将被安装为目标路径下的 abc , 如果目录名以 / 结尾, 代表将这个目录中的内容安装到目标路径,但不包括这个目录本身。 PATTERN 用于使用正则表达式 进行过滤, PERMISSIONS 用于 指定 PATTERN 过滤后的文件 权限。 我们来看一个例子 :

INSTALL(DIRECTORY icons scripts/ DESTINATION share/myproj PATTERN "CVS" EXCLUDE PATTERN "scripts/*" PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ)

4

1 INSTALL ( DIRECTORY

icons

scripts /

DESTINATION

share / myproj

PATTERN

"CVS"

EXCLUDE 2

PATTERN

"scripts/*" 3

PERMISSIONS

OWNER_EXECUTE

OWNER_WRITE

OWNER_READ 4

GROUP_EXECUTE

GROUP_READ ) 这条指令的执行结果是: 将 icons 目录安装到 <prefix> /share/myproj ,将 scripts/ 中的内容安装到 <prefix> /share/myproj 不包含目录名为 CVS 的目录,对于 scripts/* 文件指定权限为 OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ.

安装时 CMAKE 脚本的执行:

INSTALL([[SCRIPT <file>] [CODE <code>]] [...])

1

1 INSTALL ([[ SCRIPT

< file

] [ CODE

< code

]] [...]) SCRIPT 参数用于在安装时 调用 cmake 脚本文件 (也就是 <abc> .cmake 文件) CODE 参数用于 执行 CMAKE 指令 ,必须以双引号括起来。比如: INSTALL(CODE "MESSAGE("Sample install message.")")

1

1 INSTALL ( CODE

"MESSAGE("Sample install message.")" )

一个疑问 如果我没有定义 CMAKE_INSTALL_PREFIX 会安装到什么地方? 你可以尝试以下, cmake ..;make;make install ,你会发现 CMAKE_INSTALL_PREFIX 的默认定义是 /usr/loca

ADD_LIBRARY

ADD_LIBRARY(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL] source1 source2 ... sourceN)

3

1 ADD_LIBRARY ( libname [ SHARED | STATIC | MODULE ] 2 [ EXCLUDE_FROM_ALL ] 3

source1

source2 ... sourceN ) 你不需要写全 libhello.so , 只需要填写 hello 即可 , cmake 系统会自动为你生成 libhello.X 类型有三种 : SHARED ,动态库 STATIC ,静态库 MODULE ,在使用 dyld 的系统有效,如果不支持 dyld ,则被当作 SHARED 对待。 EXCLUDE_FROM_ALL 参数的意思是这个库 不会被默认构建 ,除非有其他的组件依赖或者手 工构建。

SET_TARGET_PROPERTIES

其基本语法是:

SET_TARGET_PROPERTIES(target1 target2 ... PROPERTIES prop1 value1 prop2 value2 ...)

3

1 SET_TARGET_PROPERTIES ( target1

target2 ... 2

PROPERTIES

prop1

value1 3

prop2

value2 ...) (这条 命令 要 写在add_library后面 ) 这条指令可以用来 设置输出的名称 ,对于动态库,还可以用来指定动态库版本和 API 版本。 在本例中,我们需要作的是向 lib/CMakeLists.txt 中添加一条: SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello") 这样,我们就可以同时得到 libhello.so/libhello.a 两个库了。

与他对应的指令是: GET_TARGET_PROPERTY(VAR target property)

1

1 GET_TARGET_PROPERTY ( VAR

target

property )

具体用法如下例,我们向 lib/CMakeListst.txt 中添加: GET_TARGET_PROPERTY(OUTPUT_VALUE hello_static OUTPUT_NAME) MESSAGE(STATUS "This is the hello_static OUTPUT_NAME:"${OUTPUT_VALUE})

2

1 GET_TARGET_PROPERTY ( OUTPUT_VALUE

hello_static

OUTPUT_NAME ) 2 MESSAGE ( STATUS

"This is the hello_static OUTPUT_NAME:" $ { OUTPUT_VALUE })

如果 没有 这个 属性定义 ,则 返回 NOTFOUND .

清理生成的动态库或静态库 SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1) SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

2

1 SET_TARGET_PROPERTIES ( hello

PROPERTIES

CLEAN_DIRECT_OUTPUT

1 ) 2 SET_TARGET_PROPERTIES ( hello_static

PROPERTIES

CLEAN_DIRECT_OUTPUT

1 )

动态库版本号 按照规则,动态库是应该包含一个版本号的,我们可以看一下系统的动态库,一般情况是 libhello.so.1.2 libhello.so ->libhello.so.1 libhello.so.1->libhello.so.1.2 为了实现动态库版本号,我们仍然需要使用 SET_TARGET_PROPERTIES 指令。 具体使用方法如下: SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)

1 SET_TARGET_PROPERTIES ( hello

PROPERTIES

VERSION

1.2

SOVERSION

1 ) VERSION 指代动态库版本, SOVERSION 指代 API 版本 。 将上述指令加入 lib/CMakeLists.txt 中,重新构建看看结果。 在 build/lib 目录会生成: libhello.so.1.2 libhello.so.1->libhello.so.1.2 libhello.so ->libhello.so.1