cmake 预备知识
- 入门cmake,可以查阅 Cmake Practice文档,
链接: https://pan.baidu.com/s/1hVzkJPaf6SPpX5SCTjdJYQ
提取码:gbf3 - cmake 官网手册链接:https://cmake.org/documentation/
- 早些时候,为了保证轻量级,很多开源的代码都是使用 autotool 工具进行工程构建, 通过
./configure && make && make install
编译执行,而不会采用比较臃肿的工具去实现构建, GNU Autotools框架是由三个主要的包组成,每个包都提供和依赖几个更小的组件。这三个主要的包是Autoconf,Automake和Libtool,这些工具可以屏蔽底层的不同操作系统的差异,库版本的差异。但是这种工具在使用上还是过于繁琐,因此,笔者尝试使用cmake去进行相应的代替。 - 当然, 简单的编译代码的时候, 我们可以直接借助gcc以及g++的命令, 也可以写一下简单Makefile, 简单的Makefile文件还是比较容易编写的,但是复杂的Makefile文件就挺麻烦的,比如我们依赖的头文件在哪里,我们依赖的库应该怎么写, 所以较大的工程以及可移植性,比较需要维护性的场合就要求我们不得不去借助一些项目构建工具。
- makefile通常需要结合shell脚本,这几乎能够解决所有的编译问题,makefile认为程序猿能够自己管理好工程,而cmake认为 这样管理太麻烦,为了简便和少掉头发, 我们采取cmake来生成makefile编译代码,进行项目管理,跨平台。cmake中如果需要执行shell脚本或者指令的话, 可以通过
execute_process
指令。 - Qmake 是为 Qt 量身打造的,使用起来非常方便, cmake 使用上不如qmake简单直接,但复杂换来的是强大的功能。
- 在C/C++中,我们可以通过gcc/g++进行代码编译, 也可以通过objdump进行反汇编调试。
objdump -S -d exe_file > hex_file
- 在makefile中, 我们常见的几种变量包括cflags,ldflags, extra_cflags等。 这些变量的命令原则其实都是大家约定俗成的命名规范。其作用原理如下: 当我们通过make工具编译makefile的时候, 由makefile外部定义的变量会传递到makefile内部,然后覆盖内部变量的值, 这样就能达到传递值的作用,比如
make CFLAGS="c_flags"
。 make all, 其实就是相当于执行makefile文件内部中的all目标, 当然我们创建clean目标,或者install目标。
- configure工具可以指定各种参数去生成makefile, 类似cmake工具,其主要由shell脚本组成, 并且会借助ldconfig工具进行库的连接, 有时候在新编译某个库之后, 会出现连接不到库的情况, 记得用
ldconfig
命令刷新一下查找结果。# 有时候多个库冲突的时候,ldconfig会没有准确找到某个库 $ vim /etc/ld.so.conf #添加以下配置: /usr/local/protobuf/lib $ ldconfig $ GNU的configure编译的常见形式就是: autogen.sh && ./configure {params} && make check && make
- 交叉编译通常指的是在x86架构的机器上,编译生成arm架构上的二进制文件。
Build platform: The platform on which the compilation tools are executed,编译工具执行的地方
Host platform: The platform on which the code will run,代码直接可以运行,但是其运行的环境就叫host
Target platform: Only when building a compiler, this is the platform for which the compiler will generate code,构建编译器的环 境。 - makefile可以定义函数,在通过call命令去执行不同的函数, call命令同时也支持传递参数进去到makefile函数内部。
all: a b c (最终产生的二进制目标文件) a: (a目标需要的动作) b: (b目标需要的动作) 以上即可以编译出多个可执行文件,多个目标 gcc -c 的时候,可以使用 %.o: %.c gcc -c $@ $^(调用makefile中目标, 依赖标识符) %的通配符会跟据需要的.o 去编译相应的.c
gcc\g++ 通过-I(大写的i) -L 来添加库和头文件的具体路径目录, 再使用 -l(小写的L) , 来添加要链接的库名。
例如: lconfig 后面紧接着的,就是库的名字,不过这也是有限制的, 这个库必须在/usr/local/lib, /usr/lib, /lib下才可以这么做,
注意!!!而如果不在的话, 则需要使用 -L 来指定查找 -l库名的路径。
所以一般情况下就是
gcc -o a a.c -I 头文件 -l库名
, 实在无法ld的话, 使用-L 来指定一些不在标准路径的库的路径!, gcc在编译多个c文件的目标需要的依赖时,要都添加到依赖上去, 不然会报错有声明未定义。而在cmake中,我们用target_link_libraries 和include将目录传递进去,借助cmake就会生成相对应的makefile!- Repetition is the mother of all learning 重复是学习之母
- make工具编译makefile的时候,其实就是根据文件时间戳来决定要不要编译的,所以有时候复制的代码的时候,如果比系统时间来的超前,就会出现makefile无法确定是否继续去编译的情况。
cmake常用的使用步骤:
- 常见的代码构建方式便是out of source
- 需要为任何代码工程目录建立CMakeLists.txt
- add_subdirectory 源码路径 输出路径
输出路径为使用cmake命令所在位置, 在此位置输出二进制文件的路径- 外部构建的时候,默认的输出路径指的是cmake等编译命令所在的路径,源码输入路径指的是CMakeLists.txt文件所在的路径。
- 在哪里add excutable 就在哪里set(executable)指定输出的二进制文件的路径。
- cmake …或者. ,可以指出cmakelists.txt所在的路径。只要给出路径,cmake会自己找到cmaklist.txt。
- 在哪里输出target 即 addexecutable,就在哪里的cmakelist使用install命令
- findpackage 去找.cmake模块文件,会有包含库和头文件的具体路径的变量输出, 这样,我们程序上就可以直接
include_directories()以及target_link_libraries().- cmake中 CMakelist 的if判断只需要的变量名,不需要 通过${ var}调用(这是一个cmake中的取变量值动作,和shell有点类似,变量通过花括号调用,执行指令通过圆括号调用。)
- add_exe和add_library 生成文件和库,写代码调用别人的库时,通过targetlink libraries去指定 非标准库。
cmake 语法知识点:
如何读懂官网的指令教程:
Cmake其实可以理解为纯粹的解析文本与生成makefile工具,最大作用就是简化makefile的命令操作。
如下命令,其中,<PackageName>以及 [小写字符串] 表示要自己填充值取代字符串, [大写字符串]表示cmake自带的参数, 选择该值并设置。
find_package(<PackageName> [version] [EXACT] [QUIET]
[REQUIRED] [[COMPONENTS] [components...]]
[OPTIONAL_COMPONENTS components...]
[CONFIG|NO_MODULE]
[NO_POLICY_SCOPE]
[NAMES name1 [name2 ...]]
[CONFIGS config1 [config2 ...]]
[HINTS path1 [path2 ... ]]
[PATHS path1 [path2 ... ]]
[PATH_SUFFIXES suffix1 [suffix2 ...]]
[NO_DEFAULT_PATH]
[NO_PACKAGE_ROOT_PATH]
[NO_CMAKE_PATH]
[NO_CMAKE_ENVIRONMENT_PATH]
[NO_SYSTEM_ENVIRONMENT_PATH]
[NO_CMAKE_PACKAGE_REGISTRY]
[NO_CMAKE_BUILDS_PATH] # Deprecated; does nothing.
[NO_CMAKE_SYSTEM_PATH]
[NO_CMAKE_SYSTEM_PACKAGE_REGISTRY]
[CMAKE_FIND_ROOT_PATH_BOTH |
ONLY_CMAKE_FIND_ROOT_PATH |
NO_CMAKE_FIND_ROOT_PATH])
常用知识点:
-
ADD_DEFINITIONS向 C/C++编译器添加-D 定义,如果要添加其他的编译器开关,可以通过CMAKE_C_FLAGS 变量和 CMAKE_CXX_FLAGS 变量设置。
比如:
ADD_DEFINITIONS(-DENABLE_DEBUG -DABC)
,参数之间用空格分割。
如果你的代码中定义了#ifdef ENABLE_DEBUG ... #endif
-
通过ADD_COMPILE_OPTIONS添加C/C++编译选项, 比如关闭警告等等之类的。
-
CMAKE_MINIMUM_REQUIRED
其语法为 CMAKE_MINIMUM_REQUIRED(VERSION versionNumber [FATAL_ERROR])
比如1 :
CMAKE_MINIMUM_REQUIRED(VERSION 2.5 FATAL_ERROR),如果 cmake 版本小与 2.5,则出现严重错误,整个过程中止。
比如2:CMake Error at /usr/local/lib/cmake/Boost-1.74.0/BoostConfig.cmake:240 (if): if given arguments:
“ALL” “IN_LIST” “Boost_FIND_COMPONENTS” Unknown arguments specified
该问题主要因为boost的库需要更高的cmake支持,比如3.5版本, 否则就会报这种语法错误。 -
ADD_DEPENDENCIES定义 target 依赖的其他 target,确保在编译本 target 之前,其他的 target 已经被构
建。ADD_DEPENDENCIES(target-name depend-target1 depend-target2 ...)
, cmake主要以target为驱动工作,例如makefile中定义的目标。 -
EXEC_PROGRAM,该命令可以在 CMakeLists.txt 处理过程中执行Linux命令,并不会在生成的 Makefile 中执行。
具体语法为:EXEC_PROGRAM(
Executable [directory in which to run]
[ARGS (arguments to executable)]
[OUTPUT_VARIABLE var]
[RETURN_VALUE var])
用于在指定的目录运行某个程序,通过 ARGS 添加参数,如果要获取输出和返回值,可通过
OUTPUT_VARIABLE 和 RETURN_VALUE 分别定义两个变量.
这个指令可以帮助你在 CMakeLists.txt 处理过程中支持任何命令,比如根据系统情况去
修改代码文件等等。
比如:
EXEC_PROGRAM(ls ARGS “*.c” OUTPUT_VARIABLE LS_OUTPUT RETURN_VALUE LS_RVALUE) -
FILE指令
文件操作指令,基本语法为: FILE(WRITE filename “message to write”… ) -
INCLUDE 指令,用来载入 CMakeLists.txt 文件,也用于载入预定义的 cmake 模块,如果定义的是一个模块,那么将在 CMAKE_MODULE_PATH 中搜索这个模块并载入。
INCLUDE(file1 [OPTIONAL])
INCLUDE(module [OPTIONAL]) -
使用cmake工具的时候, 一定要注意通过外部构建(out-of-source build)的方式构建工程, 避免产生的临时文件污染工程代码,并且cmake中大部分内置变量也正是为了区分二者而产生, 比如,EXECUTABLE_OUTPUT_PATH等等, 一般可以这样理解, 那就是执行cmake和make命令的目录,作为中间文件和二进制文件的输出目录, 通过
cmake ..
以及cmake .
来指定工程开始构建的入口目录, 来让cmake找到CMakeLists.txt的所在目录, 类似ADD_SUBDIRECTORY(src bin)
命令,src目录会被在CMakeLists.txt所在的目录下被查找到
,bin目录
会被在cmake和make执行的目录下被创建, 总结一下, 即构建代码时,一般指定源目录的参数变量时候,会在CMakeLists.txt的目录下查找, 输出目录(用于输出产生中间文件的路径)会在cmake和make执行的目录下被创建。 -
MESSAGE(STATUS “This is BINARY dir ${HELLO_BINARY_DIR}”)
这里需要特别解释的是作为工程名的 HELLO 和生成的可执行文件 hello 是没有任何关系的。
注意, message中的status要大写才能生效, 否则会被当做是普通字符。 (在makefile中,用warning 打印变量.) -
使用了
${}
来引用变量,这是 cmake 的变量应用方式,但是,有一些例外,比
如在 IF 控制语句,变量是直接使用变量名
引用,而不需要${var}
。如果使用了${var}
去应用变
量,其实 IF 会去判断名为${var}
所代表的值的变量,那当然是不存在的了。 -
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在使用上是等价的。
4, 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”), 另外需要注意的是 SET指令可以设置多个值,比如本例子中,可以设置 多个代码文件的文件名给SRC_LIST变量, 最终,ADD_EXECUTABLE会去查找文件以及生成makfile ,但是多次执行SET指令时, 如果设置的同名的变量, 则变量的值会被二次覆盖,
5, 使用ADD_EXECUTABLE的时候, 你可以可以忽略掉 source 列表中的源文件后缀,比如可以写成
ADD_EXECUTABLE(t1 main)
,cmake 会自动的在本目录查找 main.c 或者 main.cpp等 -
跟经典的 autotools 系列工具一样,cmake通过运行:
make clean
的命令来执行makefile中的clean目标来清理工程,但是make distclean
无效, cmake 无法跟踪构建产生的中间文件, 因此生成的makefile 不会包含distclean的目标。 -
构建工程中经常使用的一个命令是
ADD_SUBDIRECTORY(src bin)
, 这个命令会在CMakeLists.txt所在的目录下, 找到文件夹名为src的源码目录,进行编译,然后会创建一个名为bin文件夹的文件夹到build目录(执行cmake和make的目录)来放置所有的中间结果和目标二进制,如果不进行 bin 目录的指定,那么编译结果(包括中间结果)都将存放在 build/src 目录(这个目录跟原有的 src 目录对应)。ADD_SUBDIRECTORY 指令:
ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
这个指令用于向当前工程添加存放源文件的子目录以便编译,并可以指定中间二进制和目标二进制存放的位置。EXCLUDE_FROM_ALL 参数的含义是将这个目录从编译过程中排除,比如,工程的 example,可能就需要工程构建完成后,再进入 example 目录单独进行构建(当然,你也可以通过定义依赖来解决此类问题)。还有一个类似的命令如, SUBDIRS(dir1 dir2…),但是这个指令已经不推荐使用,SUBDIRS(src), 会将src中源码编译,然后会在编译的目录,创建src目录来放置中间结果和目标二进制。
-
不论是 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)
<projectname>_BINARY_DIR 和 PROJECT_BINARY_DIR 变量,
指的是构建和编译动作发生的当前目录, 比如外部构建时,在创建的build文件夹下执行cmake, 问题是,我应该把这两条指令写在工程的 CMakeLists.txt 还是 src 目录下的CMakeLists.txt,把握一个简单的原则,在哪里 ADD_EXECUTABLE 或 ADD_LIBRARY,如果需要改变目标存放路径,就在哪里加入上述的SET定义,来改变二进制和库文件的输出目录。 -
编译安装: 这里需要引入一个新的 cmake 指令 :INSTALL 和一个非常有用的变量CMAKE_INSTALL_PREFIX。
CMAKE_INSTALL_PREFIX 变量类似于 configure 脚本的 –prefix,常见的使用方法看起来是这个样子:
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
,其中使用cmake时, 参数为(..
表示父级目录,.
表示当前目录)
在CMakeLists.txt的文件中,通过INSTALL 指令用于定义安装规则,安装的内容可以包括目标二进制、动态库、静态库以及
文件、目录、脚本等。注意: CMAKE_INSTALL_PREFIX 的默认定义是/usr/local
INSTALL支持目标文件的安装, 目录的安装, 普通文件的安装,脚本文件的安装。
目标文件的安装:
INSTALL(TARGETS targets…
[[ARCHIVE|LIBRARY|RUNTIME] # 选择目标文件的类型
[DESTINATION dir]
[PERMISSIONS permissions…] # 安装的权限
[CONFIGURATIONS
[Debug|Release|…]] # 安装的类型
[COMPONENT {component}] # 给安装的库进行组件命名, 如COMPONENT com_a,未指定的话, 则系统默认指定一个。
[OPTIONAL]
] […])
参数中的 TARGETS 后面跟的就是我们通过 ADD_EXECUTABLE 或者 ADD_LIBRARY 定义的目标文件,可能是可执行二进制、动态库、静态库。
目标类型也就相对应的有三种,ARCHIVE 特指静态库,LIBRARY 特指动态库,RUNTIME特指可执行目标二进制。
DESTINATION 定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这时候CMAKE_INSTALL_PREFIX其实就无效了。
如果你希望使用 CMAKE_INSTALL_PREFIX 来定义安装路径,就要写成相对路径,即不要以/开头,那么安装后的路径就是CMAKE_INSTALL_PREFIX/<DESTINATION 定义的路径>。
举个例子:
INSTALL(TARGETS myrun mylib mystaticlib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION libstatic
)
上面的例子会将:
可执行二进制 myrun 安装到CMAKE_INSTALL_PREFIX/bin 目录
动态库 libmylib 安装到CMAKE_INSTALL_PREFIX/lib 目录
静态库 libmystaticlib 安装到CMAKE_INSTALL_PREFIX/libstatic 目录
特别注意的是你不需要关心 TARGETS 具体生成的路径,只需要写上 TARGETS 名称就可以了。
非目标文件的可执行程序安装(脚本之类的):
INSTALL(PROGRAMS files… DESTINATION dir
[PERMISSIONS permissions…]
[CONFIGURATIONS [Debug|Release|…]]
[COMPONENT component]
[RENAME name] [OPTIONAL])
需要区分的是:
安装时, 如果需要执行CMAKE 脚本 :
INSTALL([[SCRIPT file] [CODE code]] […])
SCRIPT 参数用于在安装时调用 cmake 脚本文件(也就是abc.cmake 文件)
CODE 参数用于执行 CMAKE 指令,必须以双引号括起来。比如:
INSTALL(CODE “MESSAGE(“Sample install message.”)”)
目录的安装:
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…]] […])
DIRECTORY 后面连接的是所在 Source 目录的相对路径,但务必注意:
abc 和 abc/有很大的区别。
如果目录名不以/结尾,那么这个目录将被安装为目标路径下的 abc,如果目录名以/结尾,
代表将这个目录中的内容安装到目标路径,但不包括这个目录本身。
-
生成库文件:通过ADD_LIBRARY命令。
ADD_LIBRARY(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL] source1 source2 ... sourceN)
liname即为我们要安装的库文件的名字,你不需要写全 libhello.so,只需要填写 hello 即可,cmake 系统会自动为你生成
libhello.X。 在生成静态库的时候, 要注意一点, 按照一般的习惯,静态库名字跟动态库名字应该是一致的,只不过后缀是.a 罢了。
9.1 下面我们用这个指令再来添加静态库:
ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})然后再在 build 目录进行外部编译,我们会发现,静态库根本没有被构建,仍然只生成了一个动态库。因为 hello 作为一个 target 是不能重名的,cmake中创建target的话,库文件和可执行文件等目标不可重名, 我们已经用过ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})去生成动态库了, 所以,静态库构建指令无效。
9.2 生成静态库文件: 如果我们需要生成与动态库同名的静态库文件,需要借助以下指令SET_TARGET_PROPERTIES,其基本语法是:
SET_TARGET_PROPERTIES(target1 target2 … PROPERTIES prop1 value prop2 value2 …),这之中有一个知识点,我们需要留意, 我们在做ADD_LIBRARY的操作的时候, 目标一般被用来当做输出的库的文件名,但是我们可以使用该命令来修改目标的属性, 包括输出的库的文件名。ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC}) #目标不可同名,则生成以hello_static为目标的库文件。 SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello") # 修改目标的输出文件名。 # cmake 生成库文件的时候, 会先清除同名的库文件,再生成目标文件。 # 设置防止清除同名的属性,才能保证动态库文件和静态库文件都同时生成。 SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1) SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1) SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1) # 设置库文件的版本信息。 GET_TARGET_PROPERTY(OUTPUT_VALUE hello OUTPUT_NAME) # 获取hello目标的OUTPUT_NAME属性,赋值到 OUTPUT_VALUE变量。
-
为目标指定头文件以及库文件。
#引入头文件的搜索路径: INCLUDE_DIRECTORIES(/usr/include/hello) # 引入库文件的搜索路径: LINK_DIRECTORIES(/usr/lib/hello directory1 directory2 ...) # src/CMakeLists.txt 中添加如下指令: TARGET_LINK_LIBRARIES(main hello) # 也可以写成 TARGET_LINK_LIBRARIES(main libhello.so) # 将 TARGET_LINK_LIBRRARIES 指令修改为: TARGET_LINK_LIBRARIES(main libhello.a)
注意: TARGET_LINK_LIBRARIES命令根据官网的说明来看, 不仅支持直接写库文件的名字, 也也支持写入库所在位置的全路径, 其实,FIND_LIBRARY命令也是返回一个包含全路径的值到变量中。
, 参考链接: https://cmake.org/cmake/help/latest/command/target_link_libraries.html -
有一种办法除了本工程之外,所有的工程都将会受益。
, 就是设置特殊的环境变量 CMAKE_INCLUDE_PATH 和 CMAKE_LIBRARY_PATH。
务必注意,这两个是环境变量而不是 cmake 变量, 在使用的过程中,如果需要添加修改之类的, 要通过export去进行设置。
同时要配合 FIND_PATH(myHeader NAMES hello.h PATHS /usr/include)。
如果你不使用 FIND_PATH,CMAKE_INCLUDE_PATH 变量的设置是没有作用的,你不能指望它会直接为编译器命令
添加参数-I[CMAKE_INCLUDE_PATH]。
以此为例,CMAKE_LIBRARY_PATH 可以用在 FIND_LIBRARY 中,然后通过LINK_DIRECTORIES(directory1 directory2 …)
将路径加载到cmake环境变量中, 同样,因为这些变量直接为 FIND_指令所使用,所以所有使用 FIND_指令的 cmake 模块都会受益。
使用举例:FIND_PATH(myHeader hello.h) #将查找到某个文件的路径添加到指定的变量中。 IF(myHeader) INCLUDE_DIRECTORIES(${myHeader}) # 查找到之后,再执行包含动作。 ENDIF(myHeader)
FIND_系列指令主要包含一下指令:
FIND_FILE(VAR name1 path1 path2 …) #VAR 变量代表找到的文件全路径,包含文件名
FIND_LIBRARY(VAR name1 path1 path2 …) #VAR 变量表示找到的库全路径,包含库文件名
FIND_PATH(VAR name1 path1 path2 …) # VAR 变量代表包含这个文件的路径。
FIND_PROGRAM(VAR name1 path1 path2 …) # VAR 变量代表包含这个程序的全路径,
###该命令的实现与使用较为复杂,后面再详细展开。
FIND_PACKAGE(name [major.minor] [QUIET] [NO_MODULE] [[REQUIRED|COMPONENTS] [componets…]])
#COMPONENT 的值就是 INSTALL时安装库的时候指定的设定的COMPONENT,相当于给库文件起的别名, 可直接用于target_link_libraries的指令当中。 -
cmake脚本文件 一般用来设置库文件和头文件的目录所在路径的变量。
除了使用find_package的方式, 我们也可以通过以下命令去实现模块的设置。
SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
设置cmake的查找的路径。
这时候你就可以通过 INCLUDE 指令来调用自己的模块了。 -
使用$ENV{NAME}指令就可以调用系统的环境变量了。
cmake 易错点
易犯错的示例:
-
目标名不可重复, 我们cmake指令中设置的目标具有很多属性, 比如目标的输出文件的文件名等, 我们可以通过专门的指令去获取, 这点可以在之后的使用需求中, 再去做额外的扩展。
目标
-
在funciton中定义的变量, 比如,通过set和 list 操作来设置的变量都是局部作用域,出了funtction便失效了。
-
cmake_parse_argument 命令的基本使用形式
cmake_parse_arguments(<prefix> <options> <one_value_keywords> <multi_value_keywords> <args>...)
prefix 的值将成为最终解析得到的变量名的前缀, 如下代码将会得到PROJECT_INCLUDE_DIRS,notice:
prefix与one_value_keywords 组成了最终的变量名,
options 加入对应的函数参数的控制操作。
one_value_keywords 为最终变量名的后缀, 从args取出对应顺序的位置的值。
multi_value_keywords 为最终变量名的后缀,从args中取出对应顺序后的多个变量的值( 当该位置只有一个keyword时,而args中仍有多个值未被取出来赋值给变量名时,此时变量名就能够获取多个多个值。)代码:
function(_argv) cmake_parse_arguments(PROJECT "SKIP_CMAKE_CONFIG_GENERATION;SKIP_PKG_CONFIG_GENERATION" "" "INCLUDE_DIRS;LIBRARIES;CATKIN_DEPENDS;DEPENDS;CFG_EXTRAS;EXPORTED_TARGETS" ${ARGN}) endfucntion()
-
CMAKE_BINARY_DIR
PROJECT_BINARY_DIR
projectname_BINARY_DIR
这三个变量指代的内容是一致的,如果是 in source 编译,指得就是工程顶层目录,如果
是 out-of-source 编译,指的是工程编译发生的目录。PROJECT_BINARY_DIR 跟其他
指令稍有区别,现在,你可以理解为他们是一致的。
CMAKE_SOURCE_DIR
PROJECT_SOURCE_DIR
projectname_SOURCE_DIR
这三个变量指代的内容是一致的,不论采用何种编译方式,都是工程顶层目录。
也就是在 in source 编译时,他跟 CMAKE_BINARY_DIR 等变量一致。
PROJECT_SOURCE_DIR 跟其他指令稍有区别,现在,你可以理解为他们是一致的。
总结: 输出文件的目录创建在编译发生的目录下, 导入代码等文件的目录在CMakeList.txt所在的目录(也就是工程目录)查找。
-
CMAKE_CURRENT_SOURCE_DIR
指的是当前处理的 CMakeLists.txt 所在的路径,比如上面我们提到的 src 子目录。 -
EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH
分别用来重新定义最终结果的存放目录,前面我们已经提到了这两个变量。 -
CMAKE_MODULE_PATH这个变量用来定义自己的 cmake 模块所在的路径。如果你的工程比较复杂,有可能会自己编写一些 cmake 模块,这些 cmake 模块是随你的工程发布的,为了让 cmake 在处理CMakeLists.txt 时找到这些模块,你需要通过 SET 指令,将自己的 cmake 模块路径设置一下。
比如:
SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
这时候你就可以通过 INCLUDE 指令来调用自己的模块了。 -
CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE
将工程代码中 包含的头文件的目录始终至于系统头文件目录的前面,当你定义的头文件确实跟系统发生冲突时可以提供一些帮助。 -
CMAKE_INCLUDE_CURRENT_DIR自动添加 CMAKE_CURRENT_BINARY_DIR 和 CMAKE_CURRENT_SOURCE_DIR 到当前处理的 CMakeLists.txt。相当于在每个 CMakeLists.txt 加入:INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
-
AUX_SOURCE_DIRECTORY, 该命令在不想一一添加代码文件的时候, 特别省时省力, 果然人类的一切设计都是为了满足惰性。
基本语法是:AUX_SOURCE_DIRECTORY(dir VARIABLE) 作用是发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表。因为目前 cmake 还不能自动发现新添加的源文件。
比如:
AUX_SOURCE_DIRECTORY(. SRC_LIST)
ADD_EXECUTABLE(main ${SRC_LIST})
你也可以通过后面提到的 FOREACH 指令来处理这个 LIST。 FOREACH的语法在很多构建工具中都会用到。 -
在g2o中,可以用find_library命令来为我们提供每个so文件的路径, 使用如find_library(var names paths) 就是在paths路径下,将与name相匹配的库的路径, 最后找到的路径,传递给var使用。cmake中如果因为系统装了很多个版本的第三方包,可以在target_link_libraries中添加本工程项目所需的库的绝对路径,include_directories()命令通过AFTER或BEFORE选项来指定将路径添加到所有路径的前面或者后面 ,要先设置CMAKE_INCLUDE_DIRECTORIES_BEFORE 变量为 ON,然后再设置include_directories()命令的属性为AFTER或BEFORE
-
cmake中通过在命令行指定-Dcmake_install_prefix变量为安装路径。 如果没指定,则默认安装到usr/local中。
-
给某个文件路径添加属性, 如ADDITIONAL_MAKE_CLEAN_FILES 属性, 该属性会在执行make clean的时候,做一些额外的删除动作, 比如说本例中删除源码。。。。。
set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES ${CMAKE_SOURCE_DIR}/${PROJECT_NAME})
CMAKE中find_package命令使用详解:
- 在CMakeLists.txt中的指令的相关使用例子如下所示:
对于系统预定义的 Find<name>.cmake 模块,使用方法一般如上例所示, 我们也可以通过INCLUDE命令去包含模块。FIND_PACKAGE(CURL) IF(CURL_FOUND) INCLUDE_DIRECTORIES(${CURL_INCLUDE_DIR}) TARGET_LINK_LIBRARIES(curltest ${CURL_LIBRARY}) ELSE(CURL_FOUND) MESSAGE(FATAL_ERROR ”CURL library not found”) ENDIF(CURL_FOUND)
每一个模块都会定义以下几个变量:
<name>_FOUND , 该变量一般配合IF语句进行使用
<name>_INCLUDE_DIR 或者<name>_INCLUDE, 头文件所在目录。
<name>_LIBRARIES 或者 <name>__LIBRARY, 库文件所在目录。find_package( [version] [REQUIRED]
REQUIRED 表示这个包一定要找到, 没找到直接暴力退出, 如果不加该标志位, 则会报错log,但是仍会继续向下执行。 - cmake中的find_package命令,首先会先到cmake_module_path变量包含的路径下中查找package.cmake文件,从而得到_INCLUDE_DIR ,_LIBRARY 的目录以及_FOUND 的变量信息,因为eigen都是头文件,没有.cmake模块文件 ,可以直接include eigen头文件的路径或者可以把find_eigen.cmake复制到工程目录下,并用list或者set命令添加此时模块文件的路径到cmake_module_path变量中。(list的话,可以用append参数来添加新的元素element到变量中。)
易错:在CMakeLists.txt文件中的find_package(name)中写入的name参数, 与模块中的find_\<name\>.cmake文件名中name字符段相对应,这里的name的值是find_package()命令中我们传入name参数名。 比如,查找sophus模块的时候,写成sophus一开始就fail,改成了Sophus正常,因为该模块文件的名字为 Find_Sophus.cmake。
find_package.cmake 或者 package_config.cmake格式是固定的, package等于使用 find_package()命令时传入的参数字段,而后续的include和target_link_libs传入的变量则是在cmake里面定义的,一般约定为 前面find_package命令传入的名字,为name_LIBRARY或者name_INCLUDE_DIR,(如果变量名无效的话,就可能需要去对应的package功能包中的package.cmake文件里面查找。) - 首先,find_package会去cmake_module_path变量中查找,在cmake_module_path变量中查找的.cmake模块文件,
- 然后,会去/usr/share目录或者/usr/local/share目录下的cmake*/module中查找模块相对应的find_name.cmake模块文件,也就是会通过cmake工具安装时自带的/usr/share/cmake*/module的路径下去查找模块文件, 这叫模块模式。其实,以上两种办法都是去module文件夹找,只是cmake_module_path是用户可以进行set设置的工程路径,后者是cmake工具在share路径下自带的module文件夹,
注意:其实我们应该如何确定此时cmake工具查找的module的具体路径呢?通过打印/CMAKE_ROOT变量来查看,module文件夹就存放于该路径之下, 要警惕cmake2和cmake3会有不同的module文件夹,如果版本cmake版本升级了,可以把cmake2/module路径下的find_*.cmake模块复制到cmake3下的module文件夹,这样eigen就能找到啦 。
- 总结一下,cmake其实就是先到cmake_module_path变量包含的目录去查模块文件,再去自己的CMAKE_ROOT/share/cmake下module文件夹去查找,
- 助记: find_命令先找find_文件,再找config文件。, 所以,find_package(name)宏指令还会通过配置模式去查找模块文件, 即查找以package-config.cmake命名的模块文件, 其中该模块文件名中的package的值与find_package(name)指令中的name的值要相匹配,否则会找不到,配置模式相对比较灵活, 当然,配置模式相对复杂,该模式主要是为了方便用户查找自己安装一些模块文件。(该模式可以具体参考官网的说明手册)。
根据cmake官方手册,,搜索路径的形式由prefix_path/package_path组成。 在Unix下,有以下几种查找路径:
prefix/
prefix/cmake/
prefix/name*/
prefix/name*/cmake/
prefix/lib|share|lib_arch/cmake/name*/,
prefix表示前缀路径,这个前缀路径目前有默认变量CMAKE_SYSYTEM_PREFIX_PATH,该变量的值有/usr/local,/usr ,/等路径, cmake工具会约定一系列可能的安装前缀,然后在这个前缀下,加上固定几种后缀,进行暴力搜索。除了这个默认变量,也有CMAKE_PREFIX_PATH变量可以进行自定义前缀路径。
比如: 将~/PROJECT工程路径set()指令设置给CMAKE_PREFIX_PATH变量, 那我们就可以找到自己的PROJECT项目的模块文件。
例子:
opencv就是在/usr/share下的opencv路径下有个文件夹 放置这些文件。这在find_package()使用配置模式查找, 然后在
-DCMAKE_PREFIX_PATH将查找安装前缀加进去。 - 配置模式总结:先确定功能模块的安装前缀,比如opencv,我们可以在安装的时候,自定义安装的路径前缀,通过find_package()命令去前缀目录查找cmake文件,常见的几种前缀的目录如上所示。此处默认前缀通常是usr以及usr/local,/三种前缀路径, 这是CMAKE_SYSYTEM_PREFIX_PATH变量定义的,我们也可以自定义自己安装的具体路径如 ~/tmp/ 目录,通过set()设置给**-DCMAKE_PREFIX_PATH变量** 。
- cmake除了配置模式,还有一种模式为包注册模式, 这种方式其实和配置模式很像,只是通常是因为开发者不希望相关的库被安装到机器上,而只是想调用相关的第三方的功能包, 那么通过注册的方式添加当前功能包的搜索路径, 便可以让cmake有更多查找路径来找到模块配置文件,官网的链接为: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#package-registry
首先,在想要调用的功能包的CMakeList.txt文件中添加export(PACKAGE PackageName),并且安装相应的nameConfig.cmake模块文件 到build编译路径下,则编译的时候, cmake工具会将功能包当前的build编译路径添加到 ~/.cmake/packages/PackageName的路径下的某个文件中, 其中cmake会通过hash值作为文件名来存储功能包的packageConfig.cmake文件所在的路径(其实测试发现,存储的路径也就是编译发生的路径),(windows则添加到注册表当中),这样的话,使用者通过 find_package()就可以在该路径下找到对应的nameConfig.cmake模块文件, 如下所示:注意: 该模式在cmake后续更新的版本添加了诸多限制,防止随意修改注册表之类的,比如说要设置CMAKE_EXPORT_PACKAGE_REGISTRY 变量才能来启用该功能。
sino@chinaSouth:~/.cmake/packages/SickLDMRS$ ls
c0d47ba87a15445d64aa7364cf97b552 db62510b9aca12b0b91ef94e56b23485
sino@chinaSouth:~/.cmake/packages/SickLDMRS$ cat c0d47ba87a15445d64aa7364cf97b552
/home/sino/autoware.ai/build/sick_ldmrs_driver/src/driver